canvaslms 5.7__py3-none-any.whl → 5.8__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/results.nw CHANGED
@@ -39,6 +39,7 @@ import importlib.machinery
39
39
  import importlib.util
40
40
  import os
41
41
  import pathlib
42
+ import pkgutil
42
43
  import re
43
44
  import sys
44
45
 
@@ -55,13 +56,27 @@ The command requires two arguments: course and assignment.
55
56
  We also want the option to filter on users.
56
57
  We can add these by using [[add_assignment_option]], however, we don't need the
57
58
  ungraded flag as we want to export results (\ie graded material).
58
- Also, we can just add the [[add_user_or_group_option]] to be able to filter on
59
+ Also, we can just add the [[add_user_or_group_option]] to be able to filter on
59
60
  users or groups.
61
+
62
+ The epilog contains a formatted list of available grading modules.
63
+ We use [[RawDescriptionHelpFormatter]] to preserve the whitespace formatting in
64
+ the epilog while still allowing argument help text to wrap normally.
65
+
66
+ However, we must be careful about how we construct the epilog string.
67
+ When [[black]] formats the generated Python, triple-quoted strings passed as
68
+ arguments get indented on continuation lines.
69
+ Since [[RawDescriptionHelpFormatter]] preserves this whitespace literally, we'd
70
+ end up with oddly indented prose text in the help output.
71
+
72
+ To avoid this, we build the epilog via the [[format_results_epilog]] function,
73
+ which joins individual lines without introducing unwanted indentation.
60
74
  <<add results command to subp>>=
61
75
  results_parser = subp.add_parser("results",
62
76
  help="Lists results of a course",
63
77
  description="""<<results command description>>""",
64
- epilog="""<<results command epilog>>""")
78
+ epilog=format_results_epilog(),
79
+ formatter_class=argparse.RawDescriptionHelpFormatter)
65
80
  results_parser.set_defaults(func=results_command)
66
81
  assignments.add_assignment_option(results_parser, ungraded=False)
67
82
  users.add_user_or_group_option(results_parser)
@@ -83,13 +98,10 @@ prevent the student from getting a grade. Output format, CSV:
83
98
  <course code> <component code> <student ID> <missing assignment> <reason>
84
99
 
85
100
  The reason can be "not submitted" or "not graded".
86
- <<results command epilog>>=
87
- If you specify an assignment group, the results of the assignments in that
88
- group will be summarized. You can supply your own function for summarizing
89
- grades through the -S option. See `pydoc3 canvaslms.grades` for different
90
- options.
91
- @ We will cover the option for and loading of the custom summary module later,
101
+ @ We will cover the option for and loading of the custom summary module later,
92
102
  in \cref{custom-summary-modules}.
103
+ We will also cover the [[format_grading_modules_help]] function, which
104
+ dynamically discovers available modules in the [[canvaslms.grades]] package.
93
105
 
94
106
  Now, that [[results_command]] function must take three arguments: [[config]],
95
107
  [[canvas]] and [[args]].
@@ -395,18 +407,120 @@ if grade is None or grade_date is None \
395
407
  continue
396
408
  @
397
409
 
410
+ \subsection{Discovering available grading modules}
411
+ \label{grading-module-discovery}
412
+
413
+ When users install [[canvaslms]] via pipx, the package resides in an isolated
414
+ virtual environment.
415
+ This means the traditional approach of using [[pydoc3 canvaslms.grades]] to
416
+ discover available modules no longer works---pydoc searches the system Python
417
+ path, not pipx's isolated environment.
418
+
419
+ To solve this, we dynamically discover available modules at runtime using
420
+ [[pkgutil.iter_modules]].
421
+ This works regardless of installation method because we introspect from within
422
+ the package itself.
423
+ If a module fails to import (for example, due to missing dependencies), we
424
+ simply skip it rather than failing the entire discovery process.
425
+
426
+ We provide two helper functions: one to list the modules with their
427
+ descriptions, and one to format them for display in help text.
428
+ <<functions>>=
429
+ def list_grading_modules():
430
+ """
431
+ Discover available grading modules in canvaslms.grades package.
432
+ Returns a list of (module_name, description) tuples.
433
+ """
434
+ import canvaslms.grades
435
+
436
+ modules = []
437
+ # These are internal modules, not user-facing grading strategies
438
+ excluded = {"__init__", "grades"}
439
+
440
+ for importer, modname, ispkg in pkgutil.iter_modules(canvaslms.grades.__path__):
441
+ if modname in excluded:
442
+ continue
443
+ try:
444
+ module = importlib.import_module(f"canvaslms.grades.{modname}")
445
+ doc = module.__doc__
446
+ if doc:
447
+ # Take only the first line of the docstring
448
+ first_line = doc.strip().split('\n')[0]
449
+ else:
450
+ first_line = "(no description)"
451
+ modules.append((modname, first_line))
452
+ except ImportError:
453
+ continue
454
+
455
+ return sorted(modules)
456
+
457
+ def format_grading_modules_help():
458
+ """Format the list of grading modules for help text display."""
459
+ modules = list_grading_modules()
460
+ if not modules:
461
+ return " (no modules found)"
462
+
463
+ lines = []
464
+ for name, desc in modules:
465
+ lines.append(f" canvaslms.grades.{name}: {desc}")
466
+ return "\n\n".join(lines)
467
+
468
+ def format_results_epilog():
469
+ """
470
+ Build the epilog for results command help.
471
+
472
+ We construct the epilog by joining lines explicitly rather than using a
473
+ triple-quoted string. This avoids indentation issues when black formats
474
+ the generated Python code---triple-quoted strings in function arguments
475
+ get continuation-line indentation that RawDescriptionHelpFormatter
476
+ would preserve literally.
477
+ """
478
+ parts = [
479
+ "If you specify an assignment group, the results of the assignments in that",
480
+ "group will be summarized. You can supply your own function for summarizing",
481
+ "grades through the -S option.",
482
+ "",
483
+ "Available grading modules:",
484
+ format_grading_modules_help(),
485
+ "",
486
+ "You can also provide a path to your own Python file with a summarize_group",
487
+ "function."
488
+ ]
489
+ return "\n".join(parts)
490
+ @
491
+
492
+ The [[list_grading_modules]] function uses [[pkgutil.iter_modules]] to iterate
493
+ over all submodules in the [[canvaslms.grades]] package.
494
+ We exclude [[__init__]] (the package initializer) and [[grades]] (an internal
495
+ module) since these are not user-facing grading strategies.
496
+
497
+ For each module, we import it and extract the first line of its docstring as a
498
+ brief description.
499
+ This gives users immediate visibility into what each module does, right in the
500
+ help output.
501
+
502
+ The [[format_grading_modules_help]] function joins module entries with blank
503
+ lines between them (double newline) for visual separation in the help output.
504
+ This makes the list easier to scan when module descriptions vary in length.
505
+
506
+ The [[format_results_epilog]] function builds the complete epilog by joining
507
+ individual lines.
508
+ This approach avoids the pitfall where [[black]] indents continuation lines in
509
+ triple-quoted strings, which [[RawDescriptionHelpFormatter]] would preserve
510
+ literally, resulting in oddly indented prose in the help output.
511
+
512
+
398
513
  \subsection{Loading a custom summary module}
399
514
  \label{custom-summary-modules}
400
515
 
401
- Different teachers have different policies for merging several assignments into
516
+ Different teachers have different policies for merging several assignments into
402
517
  one grade.
403
518
  We now want to provide a way to override the default function.
404
519
  <<summary module option doc>>=
405
- Name of Python module or file containing module to load with a custom
406
- summarization function to summarize assignment groups. The default module is
407
- part of the `canvaslms` package: `{default_summary_module}`. But it could be
408
- any Python file in the file system or other built-in modules. See `pydoc3
409
- canvaslms.grades` for alternative modules or how to build your own.
520
+ Name of Python module or file to load with a custom summarization function.
521
+ Default: `{default_summary_module}`. Available modules: """ + \
522
+ ", ".join(m[0] for m in list_grading_modules()) + """. \
523
+ Or provide a path to your own Python file.
410
524
  <<add option for custom summary module>>=
411
525
  default_summary_module = "canvaslms.grades.conjunctavg"
412
526
  results_parser.add_argument("-S", "--summary-module",
canvaslms/cli/results.py CHANGED
@@ -12,6 +12,7 @@ import importlib.machinery
12
12
  import importlib.util
13
13
  import os
14
14
  import pathlib
15
+ import pkgutil
15
16
  import re
16
17
  import sys
17
18
 
@@ -250,6 +251,71 @@ def summarize_modules(canvas, args):
250
251
  ]
251
252
 
252
253
 
254
+ def list_grading_modules():
255
+ """
256
+ Discover available grading modules in canvaslms.grades package.
257
+ Returns a list of (module_name, description) tuples.
258
+ """
259
+ import canvaslms.grades
260
+
261
+ modules = []
262
+ # These are internal modules, not user-facing grading strategies
263
+ excluded = {"__init__", "grades"}
264
+
265
+ for importer, modname, ispkg in pkgutil.iter_modules(canvaslms.grades.__path__):
266
+ if modname in excluded:
267
+ continue
268
+ try:
269
+ module = importlib.import_module(f"canvaslms.grades.{modname}")
270
+ doc = module.__doc__
271
+ if doc:
272
+ # Take only the first line of the docstring
273
+ first_line = doc.strip().split("\n")[0]
274
+ else:
275
+ first_line = "(no description)"
276
+ modules.append((modname, first_line))
277
+ except ImportError:
278
+ continue
279
+
280
+ return sorted(modules)
281
+
282
+
283
+ def format_grading_modules_help():
284
+ """Format the list of grading modules for help text display."""
285
+ modules = list_grading_modules()
286
+ if not modules:
287
+ return " (no modules found)"
288
+
289
+ lines = []
290
+ for name, desc in modules:
291
+ lines.append(f" canvaslms.grades.{name}: {desc}")
292
+ return "\n\n".join(lines)
293
+
294
+
295
+ def format_results_epilog():
296
+ """
297
+ Build the epilog for results command help.
298
+
299
+ We construct the epilog by joining lines explicitly rather than using a
300
+ triple-quoted string. This avoids indentation issues when black formats
301
+ the generated Python code---triple-quoted strings in function arguments
302
+ get continuation-line indentation that RawDescriptionHelpFormatter
303
+ would preserve literally.
304
+ """
305
+ parts = [
306
+ "If you specify an assignment group, the results of the assignments in that",
307
+ "group will be summarized. You can supply your own function for summarizing",
308
+ "grades through the -S option.",
309
+ "",
310
+ "Available grading modules:",
311
+ format_grading_modules_help(),
312
+ "",
313
+ "You can also provide a path to your own Python file with a summarize_group",
314
+ "function.",
315
+ ]
316
+ return "\n".join(parts)
317
+
318
+
253
319
  def load_module(module_name):
254
320
  """
255
321
  Load a module from the file system or a built-in module.
@@ -334,10 +400,8 @@ def add_command(subp):
334
400
  <course code> <component code> <student ID> <missing assignment> <reason>
335
401
 
336
402
  The reason can be "not submitted" or "not graded".""",
337
- epilog="""If you specify an assignment group, the results of the assignments in that
338
- group will be summarized. You can supply your own function for summarizing
339
- grades through the -S option. See `pydoc3 canvaslms.grades` for different
340
- options.""",
403
+ epilog=format_results_epilog(),
404
+ formatter_class=argparse.RawDescriptionHelpFormatter,
341
405
  )
342
406
  results_parser.set_defaults(func=results_command)
343
407
  assignments.add_assignment_option(results_parser, ungraded=False)
@@ -362,11 +426,11 @@ def add_command(subp):
362
426
  "--summary-module",
363
427
  required=False,
364
428
  default=default_summary_module,
365
- help=f"""Name of Python module or file containing module to load with a custom
366
- summarization function to summarize assignment groups. The default module is
367
- part of the `canvaslms` package: `{default_summary_module}`. But it could be
368
- any Python file in the file system or other built-in modules. See `pydoc3
369
- canvaslms.grades` for alternative modules or how to build your own.""",
429
+ help=f"""Name of Python module or file to load with a custom summarization function.
430
+ Default: `{default_summary_module}`. Available modules: """
431
+ + ", ".join(m[0] for m in list_grading_modules())
432
+ + """. \
433
+ Or provide a path to your own Python file.""",
370
434
  )
371
435
  default_missing_module = "canvaslms.cli.results"
372
436
  results_parser.add_argument(
canvaslms/grades/Makefile CHANGED
@@ -6,6 +6,7 @@ MODULES+= disjunctmax.py
6
6
  MODULES+= maxgradesurvey.py
7
7
  MODULES+= conjunctavgsurvey.py
8
8
  MODULES+= tilkryLAB1.py
9
+ MODULES+= participation.py
9
10
 
10
11
  .PHONY: all
11
12
  all: grades.tex
@@ -14,6 +15,7 @@ all: disjunctmax.tex
14
15
  all: maxgradesurvey.tex
15
16
  all: conjunctavgsurvey.tex
16
17
  all: tilkryLAB1.tex
18
+ all: participation.tex
17
19
  all: ${MODULES}
18
20
 
19
21
  grades.tex: conjunctavg.tex
@@ -21,6 +23,7 @@ grades.tex: disjunctmax.tex
21
23
  grades.tex: maxgradesurvey.tex
22
24
  grades.tex: conjunctavgsurvey.tex
23
25
  grades.tex: tilkryLAB1.tex
26
+ grades.tex: participation.tex
24
27
 
25
28
  __init__.py: init.py
26
29
  ${MV} $^ $@
@@ -135,3 +135,4 @@ We can also give the relative or absolute path to \texttt{mysum.py} instead.
135
135
  \input{../src/canvaslms/grades/disjunctmax.tex}
136
136
  \input{../src/canvaslms/grades/maxgradesurvey.tex}
137
137
  \input{../src/canvaslms/grades/tilkryLAB1.tex}
138
+ \input{../src/canvaslms/grades/participation.tex}
@@ -0,0 +1,218 @@
1
+ \section{Conjunctive P/F grading, the \texttt{participation} module}
2
+
3
+ This module implements conjunctive P/F grading for participation-based
4
+ assignments.
5
+ It's useful when students must complete \emph{all} assignments in a group to
6
+ pass, where each assignment is graded as complete/incomplete or 100/0.
7
+
8
+ As an example, consider a course where students pass by participating in
9
+ seminars and completing reflections:
10
+ \begin{minted}{text}
11
+ $ canvaslms assignments list -c vetcyb25h -A "^Participation INL1" \
12
+ | cut -f 1-3
13
+ DA2215 HT25 (vetcyb25h) Participation INL1 Reflection on literature reviews
14
+ DA2215 HT25 (vetcyb25h) Participation INL1 Overview of Science in Security
15
+ DA2215 HT25 (vetcyb25h) Participation INL1 How to Design Computer Security Experiments
16
+ DA2215 HT25 (vetcyb25h) Participation INL1 How do you know it's secure? Passwords
17
+ DA2215 HT25 (vetcyb25h) Participation INL1 Comprehension, literature review: Of passwords and people
18
+ DA2215 HT25 (vetcyb25h) Participation INL1 Comprehension: Graphical Passwords: Learning from the First Twelve Years
19
+ DA2215 HT25 (vetcyb25h) Participation INL1 Achieving Rigor in Literature Reviews Insights from Qualitative Data Analysis and Tool-Support
20
+ DA2215 HT25 (vetcyb25h) Participation INL1 Live seminar 13/11 at 13:15
21
+ DA2215 HT25 (vetcyb25h) Participation INL1 Comprehension: Of Passwords and People, Measuring the Effect of Password-Composition Policies
22
+ DA2215 HT25 (vetcyb25h) Participation INL1 Comprehension: Can long passwords be secure and usable?
23
+ DA2215 HT25 (vetcyb25h) Participation INL1 Comprehension: Why phishing works
24
+ DA2215 HT25 (vetcyb25h) Participation INL1 Live seminar 6/11 at 15:15
25
+ DA2215 HT25 (vetcyb25h) Participation INL1 The RSA and ElGamal cryptosystems
26
+ DA2215 HT25 (vetcyb25h) Participation INL1 On the Security of EIGamal Based Encryption
27
+ DA2215 HT25 (vetcyb25h) Participation INL1 Stealing Keys from PCs using a Radio: Cheap Electromagnetic Attacks on Windowed Exponentiation
28
+ DA2215 HT25 (vetcyb25h) Participation INL1 Timing Analysis of Keystrokes and Timing Attacks on SSH
29
+ DA2215 HT25 (vetcyb25h) Participation INL1 Reflection on the use of models, part I
30
+ DA2215 HT25 (vetcyb25h) Participation INL1 Theorem proving: 1. Introduction
31
+ DA2215 HT25 (vetcyb25h) Participation INL1 Theorem proving: 2. Formal methods and Interactive Theorem Proving
32
+ DA2215 HT25 (vetcyb25h) Participation INL1 Theorem proving: 4. Examples for what can be verified with Interactive Theorem Provers
33
+ DA2215 HT25 (vetcyb25h) Participation INL1 Theorem proving: 5. Limitations of Interactive Theorem Proving and Conclusion
34
+ DA2215 HT25 (vetcyb25h) Participation INL1 Model checking: algorithmic verification and debugging
35
+ DA2215 HT25 (vetcyb25h) Participation INL1 Reflection on the use of models, part II
36
+ DA2215 HT25 (vetcyb25h) Participation INL1 Live seminar 28/11 at 10:15
37
+ DA2215 HT25 (vetcyb25h) Participation INL1 Dos and Don'ts of Machine Learning in Computer Security
38
+ DA2215 HT25 (vetcyb25h) Participation INL1 Reflection on the use of statistics
39
+ DA2215 HT25 (vetcyb25h) Participation INL1 Comprehension: Why Johnny can't encrypt
40
+ DA2215 HT25 (vetcyb25h) Participation INL1 Comprehension: Comparing the Usability of Cryptographic APIs
41
+ DA2215 HT25 (vetcyb25h) Participation INL1 Reflection on qualitative methods
42
+ DA2215 HT25 (vetcyb25h) Participation INL1 SoK: Science, Security and the Elusive Goal of Security as a Scientific Pursuit
43
+ DA2215 HT25 (vetcyb25h) Participation INL1 Reflection on Science in Security
44
+ DA2215 HT25 (vetcyb25h) Participation INL1 Live seminar 12/12 at 12:15
45
+ DA2215 HT25 (vetcyb25h) Participation INL1 Tor: The Second-Generation Onion Router
46
+ DA2215 HT25 (vetcyb25h) Participation INL1 Comprehension: Users get routed: traffic correlation on tor by realistic adversaries
47
+ DA2215 HT25 (vetcyb25h) Participation INL1 Comprehension: Shadow: Running Tor in a Box for Accurate and Efficient Experimentation
48
+ DA2215 HT25 (vetcyb25h) Participation INL1 Comprehension: Website Fingerprinting with Website Oracles
49
+ DA2215 HT25 (vetcyb25h) Participation INL1 Comprehension: Online Website Fingerprinting: Evaluating Website Fingerprinting Attacks on Tor in the Real World
50
+ DA2215 HT25 (vetcyb25h) Participation INL1 Reflection on the use of models, part III
51
+ DA2215 HT25 (vetcyb25h) Participation INL1 Live seminar 3/12 at 12:15
52
+ DA2215 HT25 (vetcyb25h) Participation INL1 Reflection on inferences, experiments and measurements
53
+ \end{minted}
54
+ The exact assignments and names might change though.
55
+ But that's fine.
56
+ The grading system remains the same, we have the following grades:
57
+ \begin{minted}{text}
58
+ $ canvaslms submissions list -c vetcyb25h -A "^Participation INL1" \
59
+ | cut -f 4 | sort -u
60
+
61
+ 100
62
+ complete
63
+ incomplete
64
+ \end{minted}
65
+ We want all assignments to have either complete or 100 to get a passing grade.
66
+ Otherwise the student gets an F.
67
+
68
+ \subsection{Grading logic: conjunctive P/F}
69
+
70
+ This module implements what we call \emph{conjunctive P/F grading}.
71
+ Let's contrast this with other approaches to understand what makes it unique:
72
+ \begin{description}
73
+ \item[conjunctavg] All assignments must pass, and A--E grades are averaged.
74
+ Here we have no A--E grades, only complete/incomplete or 100/empty.
75
+ \item[disjunctmax] At least one assignment must pass (disjunctive), and we take
76
+ the best grade.
77
+ Here \emph{all} assignments must pass (conjunctive), and the only possible
78
+ grades are P or F.
79
+ \end{description}
80
+
81
+ The grading rule is simple: if \emph{all} participation assignments have a
82
+ passing grade, the student passes.
83
+ If \emph{any} assignment is missing or has an incomplete grade, the student
84
+ fails.
85
+
86
+ \subsection{Module structure}
87
+
88
+ We follow the standard module structure: a [[summarize_group]] function that
89
+ iterates over users and delegates to a [[summarize]] helper function.
90
+ <<[[participation.py]]>>=
91
+ """
92
+ <<module doc>>
93
+ """
94
+ import datetime as dt
95
+ from canvaslms.cli import results
96
+ from canvasapi.exceptions import ResourceDoesNotExist
97
+
98
+ <<helper functions>>
99
+
100
+ def summarize_group(assignments_list, users_list):
101
+ """
102
+ Summarizes participation assignments using conjunctive P/F grading.
103
+ All assignments must have 'complete' or '100' for P, otherwise F.
104
+ """
105
+ for user in users_list:
106
+ grade, grade_date, graders = summarize(user, assignments_list)
107
+ yield [user, grade, grade_date, *graders]
108
+ @
109
+
110
+ <<module doc>>=
111
+ Summarizes participation assignments using conjunctive P/F grading.
112
+ All assignments must have 'complete' or '100' for P, otherwise F.
113
+ @
114
+
115
+ \subsection{What counts as passing?}
116
+
117
+ A grade is considered passing if it is either:
118
+ \begin{itemize}
119
+ \item [[complete]] (case-insensitive, so [[Complete]] or [[COMPLETE]] also work)
120
+ \item [[100]] (a numeric grade indicating full completion)
121
+ \end{itemize}
122
+
123
+ Anything else---including [[incomplete]], [[None]], or a missing
124
+ submission---counts as not passing.
125
+ <<helper functions>>=
126
+ def is_passing_grade(grade):
127
+ """
128
+ Returns True if the grade indicates passing (complete or 100).
129
+ """
130
+ if grade is None:
131
+ return False
132
+ if isinstance(grade, str):
133
+ if grade.casefold() == "complete":
134
+ return True
135
+ if grade == "100":
136
+ return True
137
+ return False
138
+ @
139
+
140
+ \subsection{Summarizing a student's participation}
141
+
142
+ The [[summarize]] function iterates through all assignments and checks whether
143
+ each one has a passing grade.
144
+ We collect dates and graders along the way.
145
+ <<helper functions>>=
146
+ def summarize(user, assignments_list):
147
+ """
148
+ Extracts user's submissions for all participation assignments.
149
+ Returns (grade, date, graders) where grade is P if all passed, F otherwise.
150
+ """
151
+ passed = []
152
+ dates = []
153
+ graders = []
154
+
155
+ for assignment in assignments_list:
156
+ <<get submission for assignment>>
157
+ <<check if grade is passing>>
158
+ <<add graders to [[graders]] list>>
159
+ <<add date to [[dates]] list>>
160
+
161
+ <<determine final grade>>
162
+
163
+ return (final_grade, final_date, graders)
164
+ @
165
+
166
+ For each assignment, we attempt to fetch the student's submission.
167
+ If the submission doesn't exist (which can happen in rare cases), we treat it
168
+ as not passing.
169
+ <<get submission for assignment>>=
170
+ try:
171
+ submission = assignment.get_submission(user,
172
+ include=["submission_history"])
173
+ except ResourceDoesNotExist:
174
+ passed.append(False)
175
+ continue
176
+
177
+ submission.assignment = assignment
178
+ @
179
+
180
+ We extract the grade from the submission and check whether it's passing.
181
+ <<check if grade is passing>>=
182
+ grade = submission.grade
183
+ passed.append(is_passing_grade(grade))
184
+ @
185
+
186
+ We collect all graders from the submission history.
187
+ This ensures we credit everyone who participated in the grading process, not
188
+ just the last person to grade.
189
+ <<add graders to [[graders]] list>>=
190
+ graders += results.all_graders(submission)
191
+ @
192
+
193
+ For the date, we prefer the submission date but fall back to the grading date
194
+ if no submission was made (for example, oral presentations where the student
195
+ didn't submit anything).
196
+ <<add date to [[dates]] list>>=
197
+ grade_date = submission.submitted_at or submission.graded_at
198
+ if grade_date:
199
+ grade_date = dt.date.fromisoformat(grade_date.split("T")[0])
200
+ dates.append(grade_date)
201
+ @
202
+
203
+ \subsection{Determining the final grade}
204
+
205
+ The final grade is P if \emph{all} assignments passed, F otherwise.
206
+ If there are no dates (meaning the student has no activity at all), we return
207
+ [[None]] for both grade and date.
208
+ <<determine final grade>>=
209
+ if dates:
210
+ final_date = max(dates)
211
+ if all(passed):
212
+ final_grade = "P"
213
+ else:
214
+ final_grade = "F"
215
+ else:
216
+ final_date = None
217
+ final_grade = None
218
+ @
@@ -0,0 +1,70 @@
1
+ """
2
+ Summarizes participation assignments using conjunctive P/F grading.
3
+ All assignments must have 'complete' or '100' for P, otherwise F.
4
+ """
5
+
6
+ import datetime as dt
7
+ from canvaslms.cli import results
8
+ from canvasapi.exceptions import ResourceDoesNotExist
9
+
10
+
11
+ def is_passing_grade(grade):
12
+ """
13
+ Returns True if the grade indicates passing (complete or 100).
14
+ """
15
+ if grade is None:
16
+ return False
17
+ if isinstance(grade, str):
18
+ if grade.casefold() == "complete":
19
+ return True
20
+ if grade == "100":
21
+ return True
22
+ return False
23
+
24
+
25
+ def summarize(user, assignments_list):
26
+ """
27
+ Extracts user's submissions for all participation assignments.
28
+ Returns (grade, date, graders) where grade is P if all passed, F otherwise.
29
+ """
30
+ passed = []
31
+ dates = []
32
+ graders = []
33
+
34
+ for assignment in assignments_list:
35
+ try:
36
+ submission = assignment.get_submission(user, include=["submission_history"])
37
+ except ResourceDoesNotExist:
38
+ passed.append(False)
39
+ continue
40
+
41
+ submission.assignment = assignment
42
+ grade = submission.grade
43
+ passed.append(is_passing_grade(grade))
44
+ graders += results.all_graders(submission)
45
+ grade_date = submission.submitted_at or submission.graded_at
46
+ if grade_date:
47
+ grade_date = dt.date.fromisoformat(grade_date.split("T")[0])
48
+ dates.append(grade_date)
49
+
50
+ if dates:
51
+ final_date = max(dates)
52
+ if all(passed):
53
+ final_grade = "P"
54
+ else:
55
+ final_grade = "F"
56
+ else:
57
+ final_date = None
58
+ final_grade = None
59
+
60
+ return (final_grade, final_date, graders)
61
+
62
+
63
+ def summarize_group(assignments_list, users_list):
64
+ """
65
+ Summarizes participation assignments using conjunctive P/F grading.
66
+ All assignments must have 'complete' or '100' for P, otherwise F.
67
+ """
68
+ for user in users_list:
69
+ grade, grade_date, graders = summarize(user, assignments_list)
70
+ yield [user, grade, grade_date, *graders]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canvaslms
3
- Version: 5.7
3
+ Version: 5.8
4
4
  Summary: Command-line interface to Canvas LMS
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -26,8 +26,8 @@ canvaslms/cli/pages.nw,sha256=njm6oQT22ryI8Z7O62d-qivQjbMX_je5OwhgHEvtVUg,28934
26
26
  canvaslms/cli/pages.py,sha256=lW8DpMeyQQoVcu7ztSj_PdW-oUJC0HSh5mDeo8BuaRc,27713
27
27
  canvaslms/cli/quizzes.nw,sha256=xgivQGColOzayvMn3OcxJf0V7E2K2lzindzPeBqymBw,262940
28
28
  canvaslms/cli/quizzes.py,sha256=1gX16Mo49rO4j98xy_j7xSuIs0E9DcOJigB90imBNR4,198225
29
- canvaslms/cli/results.nw,sha256=T-ry1k_cHCH_nJfvPl4d9UBbIl_SxvXjBMxyYfXgyaw,22503
30
- canvaslms/cli/results.py,sha256=8ODAhC4r1ndyTHnWSSwFEeV4ab_snxTi1nkrsqqwRLg,15033
29
+ canvaslms/cli/results.nw,sha256=SQIiCwcQUO-irUD6-Ya-4QBVHo19uv5QHH7GXNvdrDg,26862
30
+ canvaslms/cli/results.py,sha256=2KGcqGzmrfqc2Jj5ddQcrm33qF_DYHoljTol4qI9pSg,16845
31
31
  canvaslms/cli/results.py.broken,sha256=njHu8mKfPHqH4daxy-4LMpO6FdUBLPHiVKFFmyH8aJQ,13047
32
32
  canvaslms/cli/submissions.nw,sha256=RCpN7p2Oe9sbMeIPv16CeUGvuesyT9Z5f2PdIdKxHy8,97760
33
33
  canvaslms/cli/submissions.py,sha256=mvXiknSAxmdvoHg6V6k8IqTNdRx2FZqBskniIDRcpw8,49196
@@ -36,7 +36,7 @@ canvaslms/cli/users.nw,sha256=WOutY4VTeyDBqOIbHk6VY_nxgJPk1eKU9CvH6nqjsE4,35236
36
36
  canvaslms/cli/users.py,sha256=YBmL7tUStFd_JPNHTIN1H_n-DmLgYv54JdQrcIRsUDk,16979
37
37
  canvaslms/cli/utils.nw,sha256=E6mIJFZc_mCWIvfCHgOPqFWbOgLKcATEJlJiGZMyuMk,16270
38
38
  canvaslms/cli/utils.py,sha256=RO3-TZzgxSC662vB34WOt4V9S1bHv6N2B9eVo3sFavY,4138
39
- canvaslms/grades/Makefile,sha256=a9IDqfVcT7JImJ4a9o3T16NHdmMQNPqwUan1eoaeWiU,811
39
+ canvaslms/grades/Makefile,sha256=NQoyTmiaW2Cw8sfM9LNYjiNN0rYxM8UmoLpDbdKHwu4,893
40
40
  canvaslms/grades/__init__.py,sha256=wDg-x-fbSsph2R0vradLXO4oDR1yG8Z-6whW2l5_pX8,1027
41
41
  canvaslms/grades/conjunctavg.nw,sha256=uaHcE7l72o66FUjK8x1KijE5YeMsU74WFS-j-ZCVBq0,4671
42
42
  canvaslms/grades/conjunctavg.py,sha256=ARLEWdtiDre8LDsL0DVKjQ9tfJslZAJept1fbhmKc9A,3006
@@ -44,10 +44,12 @@ canvaslms/grades/conjunctavgsurvey.nw,sha256=O158fPIv4y1_Qx8yB9O4wV9wMoVC5b5rVYG
44
44
  canvaslms/grades/conjunctavgsurvey.py,sha256=G-1i2BeapSsXJ9XfxEK0ShK5UxlqQpBcu9yJH9F1Sys,2730
45
45
  canvaslms/grades/disjunctmax.nw,sha256=tiPMd59OBsZyUdW_KqY-kSqQVL9I7u7tMSSyOQDa8YI,3982
46
46
  canvaslms/grades/disjunctmax.py,sha256=6dMah6yxHpJHKdO72SRh47GLC38UinMl0HVdGSIZePM,2703
47
- canvaslms/grades/grades.nw,sha256=EbO-rkqO12keY7nC_eOTGspTS0fZdOA1an9TNRumUUk,5046
47
+ canvaslms/grades/grades.nw,sha256=ldR-_7l3L5ymHJV82TBnQ1YLn2kwyD2Cw3bW--Q-iiY,5096
48
48
  canvaslms/grades/grades.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
49
  canvaslms/grades/maxgradesurvey.nw,sha256=agYMqH20K1DLZ0pB9CJPye0E1g_A-ImdGJJzO03HFcE,3263
50
50
  canvaslms/grades/maxgradesurvey.py,sha256=eEysQNgHuFM9-2vrx4h2ovmK5TsXbRI1ukRx7tsct7Y,2089
51
+ canvaslms/grades/participation.nw,sha256=pqSxNdIrAbLvSXAmxLpgLm263yPObGqbEPibZ0BAHj0,9439
52
+ canvaslms/grades/participation.py,sha256=Pex6-vYfqB33Lizn3Pkn6yZUq0l1LZG40555uXSxCj4,2054
51
53
  canvaslms/grades/tilkryLAB1.nw,sha256=xkieCGinC0sLpwsBPlOCxXjUPUiHITQzV7QTnJo3-io,10186
52
54
  canvaslms/grades/tilkryLAB1.py,sha256=_-p2DqeNZCnomCcXkUSSK4_KffJcaPwpjEBzfXlWsqg,3214
53
55
  canvaslms/hacks/Makefile,sha256=Xxk4Fw6U_QSB8KgBlSL8-vUqYIzWzDOXgN1_LNgmNdU,345
@@ -57,8 +59,8 @@ canvaslms/hacks/attachment_cache.py,sha256=LcOZqaa6jPrEJWUD-JYN5GTc3bxCbv2fr_vqu
57
59
  canvaslms/hacks/canvasapi.nw,sha256=ixmIHn4tgy-ZKtQ1rqWSw97hfY2m0qtGX0de2x89lwA,136470
58
60
  canvaslms/hacks/canvasapi.py,sha256=A-r48x7gO6143_QkuZ8n6EW66i-a2AXXr7X7oeehOAU,31868
59
61
  canvaslms/hacks/test_hacks.py,sha256=JSJNvZqHu1E_s51HsPD7yr1gC-R-xVe-tuMMAKU9Gj8,66709
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,,
62
+ canvaslms-5.8.dist-info/METADATA,sha256=Xz-kANG9wsPDHQvYbkE_0HfRM1jLN3z8qhmwA0w75pM,6078
63
+ canvaslms-5.8.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
64
+ canvaslms-5.8.dist-info/entry_points.txt,sha256=lyblfkLbodN5yb7q1c6-rwIoJPV-ygXrB9PYb5boHXM,48
65
+ canvaslms-5.8.dist-info/licenses/LICENSE,sha256=N_TKsbzzD5Ax5fWJqEQk9bkwtf394MJkNeFld4HV6-E,1074
66
+ canvaslms-5.8.dist-info/RECORD,,