canvaslms 5.9__py3-none-any.whl → 5.10__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 +10 -2
- canvaslms/cli/cli.nw +25 -2
- canvaslms/cli/quizzes.nw +58 -4
- canvaslms/cli/quizzes.py +24 -1
- canvaslms/cli/results.nw +85 -45
- canvaslms/cli/results.py +50 -44
- canvaslms/cli/users.nw +64 -1
- canvaslms/cli/users.py +4 -1
- canvaslms/grades/__init__.py +14 -0
- canvaslms/grades/conjunctavg.nw +50 -1
- canvaslms/grades/conjunctavg.py +31 -0
- canvaslms/grades/conjunctavgsurvey.nw +50 -0
- canvaslms/grades/conjunctavgsurvey.py +40 -0
- canvaslms/grades/disjunctmax.nw +59 -0
- canvaslms/grades/disjunctmax.py +38 -0
- canvaslms/grades/grades.nw +17 -3
- canvaslms/grades/maxgradesurvey.nw +58 -0
- canvaslms/grades/maxgradesurvey.py +41 -0
- canvaslms/grades/participation.nw +27 -0
- canvaslms/grades/participation.py +18 -0
- canvaslms/grades/tilkryLAB1.nw +47 -0
- canvaslms/grades/tilkryLAB1.py +36 -0
- {canvaslms-5.9.dist-info → canvaslms-5.10.dist-info}/METADATA +1 -1
- {canvaslms-5.9.dist-info → canvaslms-5.10.dist-info}/RECORD +27 -27
- {canvaslms-5.9.dist-info → canvaslms-5.10.dist-info}/WHEEL +0 -0
- {canvaslms-5.9.dist-info → canvaslms-5.10.dist-info}/entry_points.txt +0 -0
- {canvaslms-5.9.dist-info → canvaslms-5.10.dist-info}/licenses/LICENSE +0 -0
canvaslms/cli/users.nw
CHANGED
|
@@ -746,6 +746,47 @@ def test_list_users_sets_course_attribute(
|
|
|
746
746
|
assert result[0].course == mock_course
|
|
747
747
|
@
|
|
748
748
|
|
|
749
|
+
\subsubsection{Testing [[process_user_option]] with role filtering}
|
|
750
|
+
|
|
751
|
+
This is a regression test for a bug where [[args.role]] (a string) was passed
|
|
752
|
+
directly to [[filter_users]], causing character-by-character iteration that
|
|
753
|
+
matched all enrollment types.
|
|
754
|
+
|
|
755
|
+
We need to verify that [[process_user_option]] correctly converts the single
|
|
756
|
+
role string from argparse into a list before passing it to [[filter_users]].
|
|
757
|
+
<<test functions>>=
|
|
758
|
+
def test_process_user_option_filters_by_role(
|
|
759
|
+
mock_course, mock_student, mock_ta
|
|
760
|
+
):
|
|
761
|
+
"""Test that process_user_option correctly filters users by role"""
|
|
762
|
+
# Setup mock course and canvas
|
|
763
|
+
mock_course.get_users = Mock(return_value=[mock_student, mock_ta])
|
|
764
|
+
mock_canvas = Mock()
|
|
765
|
+
mock_canvas.get_courses = Mock(return_value=[mock_course])
|
|
766
|
+
|
|
767
|
+
# Setup args with role="student" (as a string, like argparse provides)
|
|
768
|
+
mock_args = Mock()
|
|
769
|
+
mock_args.course = ".*"
|
|
770
|
+
mock_args.user = ".*"
|
|
771
|
+
mock_args.role = "student" # Single string, not a list
|
|
772
|
+
|
|
773
|
+
# Call process_user_option
|
|
774
|
+
result = users_module.process_user_option(mock_canvas, mock_args)
|
|
775
|
+
|
|
776
|
+
# Verify only student is returned, not TA
|
|
777
|
+
assert len(result) == 1
|
|
778
|
+
assert result[0].id == 100 # Alice Student
|
|
779
|
+
|
|
780
|
+
# Now test with TA role
|
|
781
|
+
mock_course.get_users = Mock(return_value=[mock_student, mock_ta])
|
|
782
|
+
mock_args.role = "ta"
|
|
783
|
+
result = users_module.process_user_option(mock_canvas, mock_args)
|
|
784
|
+
|
|
785
|
+
# Verify only TA is returned
|
|
786
|
+
assert len(result) == 1
|
|
787
|
+
assert result[0].id == 200 # Bob TA
|
|
788
|
+
@
|
|
789
|
+
|
|
749
790
|
Second, we provide the most general function, [[filter_users]], which takes a
|
|
750
791
|
list of courses, a list of Canvas roles and a regex as arguments.
|
|
751
792
|
It returns the matching users.
|
|
@@ -934,13 +975,35 @@ When processing this option, we need to filter by course first, so we use the
|
|
|
934
975
|
processing from the [[courses]] module to get the list of courses matching the
|
|
935
976
|
courses options.
|
|
936
977
|
Then we simply filter all users.
|
|
978
|
+
|
|
979
|
+
\subsubsection{Converting role string to list}
|
|
980
|
+
|
|
981
|
+
The [[--role]] option (defined in [[add_user_roles_option]]) accepts a single
|
|
982
|
+
role string like [["student"]] or [["ta"]] via argparse.
|
|
983
|
+
However, [[filter_users]] and [[list_users]] expect a list of role names,
|
|
984
|
+
since they need to iterate over roles when checking if any role matches a
|
|
985
|
+
user's enrollments.
|
|
986
|
+
|
|
987
|
+
If we mistakenly pass a string directly to [[filter_users]], Python will
|
|
988
|
+
iterate over individual characters (for example, [["student"]] becomes
|
|
989
|
+
[['s']], [['t']], [['u']], [['d']], [['e']], [['n']], [['t']]) when checking
|
|
990
|
+
roles.
|
|
991
|
+
This causes all enrollment types to match, since characters like [['t']],
|
|
992
|
+
[['a']], [['e']], [['n']] appear in [["StudentEnrollment"]],
|
|
993
|
+
[["TaEnrollment"]], [["TeacherEnrollment"]], and so on.
|
|
994
|
+
|
|
995
|
+
Therefore, we must convert [[args.role]] to a single-element list before
|
|
996
|
+
passing it to [[filter_users]].
|
|
937
997
|
<<functions>>=
|
|
938
998
|
def process_user_option(canvas, args):
|
|
939
999
|
"""Processes the user option from command line, returns a list of users"""
|
|
1000
|
+
# args.role is a single string (e.g., "student"), but filter_users expects
|
|
1001
|
+
# a list of role names. Convert it to a single-element list.
|
|
1002
|
+
roles_list = [args.role] if args.role else []
|
|
940
1003
|
user_list = list(filter_users(
|
|
941
1004
|
courses.process_course_option(canvas, args),
|
|
942
1005
|
args.user,
|
|
943
|
-
roles=
|
|
1006
|
+
roles=roles_list))
|
|
944
1007
|
if not user_list:
|
|
945
1008
|
raise canvaslms.cli.EmptyListError("No users found matching the criteria")
|
|
946
1009
|
return user_list
|
canvaslms/cli/users.py
CHANGED
|
@@ -404,9 +404,12 @@ def add_user_option(parser, required=False):
|
|
|
404
404
|
|
|
405
405
|
def process_user_option(canvas, args):
|
|
406
406
|
"""Processes the user option from command line, returns a list of users"""
|
|
407
|
+
# args.role is a single string (e.g., "student"), but filter_users expects
|
|
408
|
+
# a list of role names. Convert it to a single-element list.
|
|
409
|
+
roles_list = [args.role] if args.role else []
|
|
407
410
|
user_list = list(
|
|
408
411
|
filter_users(
|
|
409
|
-
courses.process_course_option(canvas, args), args.user, roles=
|
|
412
|
+
courses.process_course_option(canvas, args), args.user, roles=roles_list
|
|
410
413
|
)
|
|
411
414
|
)
|
|
412
415
|
if not user_list:
|
canvaslms/grades/__init__.py
CHANGED
|
@@ -18,6 +18,20 @@ module must fulfil the following:
|
|
|
18
18
|
3) The return value should be a list of lists. Each list should have the
|
|
19
19
|
form `[user, grade, grade date, grader 1, ..., grader N]`.
|
|
20
20
|
|
|
21
|
+
OPTIONAL: A module may also provide a `missing_assignments` function for use
|
|
22
|
+
with the `--missing` flag. If provided, it will be used instead of the default.
|
|
23
|
+
|
|
24
|
+
- `missing_assignments(assignments_list, users_list, passing_regex=...,
|
|
25
|
+
optional_assignments=...)` should yield `(user, assignment, reason)` tuples.
|
|
26
|
+
|
|
27
|
+
- What "missing" means depends on the grading policy:
|
|
28
|
+
* Conjunctive modules (all must pass): Any non-passing assignment is missing
|
|
29
|
+
* Disjunctive modules (at least one must pass): Only report if NO passing
|
|
30
|
+
|
|
31
|
+
- Modules can define `is_passing_grade(grade)` and call the shared default
|
|
32
|
+
implementation from `canvaslms.cli.results.missing_assignments()` with the
|
|
33
|
+
`is_passing` callback parameter.
|
|
34
|
+
|
|
21
35
|
For more details, see Chapter 11 of the `canvaslms.pdf` file found among the
|
|
22
36
|
release files at:
|
|
23
37
|
|
canvaslms/grades/conjunctavg.nw
CHANGED
|
@@ -133,7 +133,7 @@ graders += results.all_graders(submission)
|
|
|
133
133
|
|
|
134
134
|
\subsection{Computing averages}
|
|
135
135
|
|
|
136
|
-
To compute the average for the A--E grades; we will convert the grades into
|
|
136
|
+
To compute the average for the A--E grades; we will convert the grades into
|
|
137
137
|
integers, compute the average, round the value to an integer and convert back.
|
|
138
138
|
<<helper functions>>=
|
|
139
139
|
def a2e_average(grades):
|
|
@@ -153,3 +153,52 @@ def int_to_grade(int_grade):
|
|
|
153
153
|
return grade_map_inv[int_grade]
|
|
154
154
|
@
|
|
155
155
|
|
|
156
|
+
|
|
157
|
+
\subsection{Finding missing assignments}
|
|
158
|
+
|
|
159
|
+
For conjunctive average grading, a student is \enquote{missing} any assignment
|
|
160
|
+
that doesn't have a passing grade.
|
|
161
|
+
Since ALL assignments must pass to get a grade, each individual failure
|
|
162
|
+
prevents the student from completing the group.
|
|
163
|
+
|
|
164
|
+
This is in contrast to disjunctive grading (see [[disjunctmax]]), where
|
|
165
|
+
a student only needs ONE passing grade---there, we wouldn't report
|
|
166
|
+
individual failing assignments as \enquote{missing} if the student already
|
|
167
|
+
passed via another assignment.
|
|
168
|
+
|
|
169
|
+
We define what counts as a passing grade for this module.
|
|
170
|
+
A grade is passing if it's one of A--E, P, or \enquote{complete}.
|
|
171
|
+
<<helper functions>>=
|
|
172
|
+
def is_passing_grade(grade):
|
|
173
|
+
"""
|
|
174
|
+
Returns True if grade is passing for conjunctive A-E grading.
|
|
175
|
+
"""
|
|
176
|
+
if grade is None:
|
|
177
|
+
return False
|
|
178
|
+
if grade in ["A", "B", "C", "D", "E", "P"]:
|
|
179
|
+
return True
|
|
180
|
+
if isinstance(grade, str) and grade.casefold() == "complete":
|
|
181
|
+
return True
|
|
182
|
+
return False
|
|
183
|
+
@
|
|
184
|
+
|
|
185
|
+
We reuse the shared [[missing_assignments]] implementation from
|
|
186
|
+
[[canvaslms.cli.results]], providing our module-specific [[is_passing_grade]]
|
|
187
|
+
function as a callback.
|
|
188
|
+
<<helper functions>>=
|
|
189
|
+
def missing_assignments(assignments_list, users_list,
|
|
190
|
+
passing_regex=None,
|
|
191
|
+
optional_assignments=None):
|
|
192
|
+
"""
|
|
193
|
+
Returns missing assignments for conjunctive average grading.
|
|
194
|
+
|
|
195
|
+
Any assignment without a passing grade (A-E, P, or complete) is missing.
|
|
196
|
+
"""
|
|
197
|
+
from canvaslms.cli import results
|
|
198
|
+
return results.missing_assignments(
|
|
199
|
+
assignments_list, users_list,
|
|
200
|
+
optional_assignments=optional_assignments,
|
|
201
|
+
is_passing=is_passing_grade
|
|
202
|
+
)
|
|
203
|
+
@
|
|
204
|
+
|
canvaslms/grades/conjunctavg.py
CHANGED
|
@@ -94,6 +94,37 @@ def int_to_grade(int_grade):
|
|
|
94
94
|
return grade_map_inv[int_grade]
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def is_passing_grade(grade):
|
|
98
|
+
"""
|
|
99
|
+
Returns True if grade is passing for conjunctive A-E grading.
|
|
100
|
+
"""
|
|
101
|
+
if grade is None:
|
|
102
|
+
return False
|
|
103
|
+
if grade in ["A", "B", "C", "D", "E", "P"]:
|
|
104
|
+
return True
|
|
105
|
+
if isinstance(grade, str) and grade.casefold() == "complete":
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def missing_assignments(
|
|
111
|
+
assignments_list, users_list, passing_regex=None, optional_assignments=None
|
|
112
|
+
):
|
|
113
|
+
"""
|
|
114
|
+
Returns missing assignments for conjunctive average grading.
|
|
115
|
+
|
|
116
|
+
Any assignment without a passing grade (A-E, P, or complete) is missing.
|
|
117
|
+
"""
|
|
118
|
+
from canvaslms.cli import results
|
|
119
|
+
|
|
120
|
+
return results.missing_assignments(
|
|
121
|
+
assignments_list,
|
|
122
|
+
users_list,
|
|
123
|
+
optional_assignments=optional_assignments,
|
|
124
|
+
is_passing=is_passing_grade,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
97
128
|
def summarize_group(assignments_list, users_list):
|
|
98
129
|
"""
|
|
99
130
|
Summarizes a particular set of assignments (assignments_list) for all
|
|
@@ -126,3 +126,53 @@ For who graded, we simply extract the list of graders from the submissions.
|
|
|
126
126
|
graders += results.all_graders(submission)
|
|
127
127
|
@
|
|
128
128
|
|
|
129
|
+
|
|
130
|
+
\subsection{Finding missing assignments}
|
|
131
|
+
|
|
132
|
+
For conjunctive average with surveys, the definition of \enquote{passing} is
|
|
133
|
+
broader than plain [[conjunctavg]]: numeric grades (points, percentages) also
|
|
134
|
+
count as passing.
|
|
135
|
+
This accommodates mandatory surveys that aren't graded A--F but still need to
|
|
136
|
+
be completed.
|
|
137
|
+
|
|
138
|
+
We define [[is_passing_grade]] to accept A--E, P, complete, and any numeric
|
|
139
|
+
value.
|
|
140
|
+
<<helper functions>>=
|
|
141
|
+
def is_passing_grade(grade):
|
|
142
|
+
"""
|
|
143
|
+
Returns True if grade is passing (includes numeric grades).
|
|
144
|
+
"""
|
|
145
|
+
if grade is None:
|
|
146
|
+
return False
|
|
147
|
+
if grade in ["A", "B", "C", "D", "E", "P"]:
|
|
148
|
+
return True
|
|
149
|
+
if isinstance(grade, str):
|
|
150
|
+
if grade.casefold() == "complete":
|
|
151
|
+
return True
|
|
152
|
+
# Numeric grades (points, percentages) count as passing
|
|
153
|
+
if (grade.isdigit()
|
|
154
|
+
or grade.replace('.', '', 1).isdigit()
|
|
155
|
+
or grade.replace('%', '', 1).isdigit()):
|
|
156
|
+
return True
|
|
157
|
+
return False
|
|
158
|
+
@
|
|
159
|
+
|
|
160
|
+
We reuse the shared implementation with our broader [[is_passing_grade]].
|
|
161
|
+
<<helper functions>>=
|
|
162
|
+
def missing_assignments(assignments_list, users_list,
|
|
163
|
+
passing_regex=None,
|
|
164
|
+
optional_assignments=None):
|
|
165
|
+
"""
|
|
166
|
+
Returns missing assignments for conjunctive average with surveys.
|
|
167
|
+
|
|
168
|
+
Any assignment without a passing grade (A-E, P, complete, or numeric) is
|
|
169
|
+
missing.
|
|
170
|
+
"""
|
|
171
|
+
from canvaslms.cli import results
|
|
172
|
+
return results.missing_assignments(
|
|
173
|
+
assignments_list, users_list,
|
|
174
|
+
optional_assignments=optional_assignments,
|
|
175
|
+
is_passing=is_passing_grade
|
|
176
|
+
)
|
|
177
|
+
@
|
|
178
|
+
|
|
@@ -77,6 +77,46 @@ def summarize(user, assignments_list):
|
|
|
77
77
|
return (final_grade, final_date, graders)
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
def is_passing_grade(grade):
|
|
81
|
+
"""
|
|
82
|
+
Returns True if grade is passing (includes numeric grades).
|
|
83
|
+
"""
|
|
84
|
+
if grade is None:
|
|
85
|
+
return False
|
|
86
|
+
if grade in ["A", "B", "C", "D", "E", "P"]:
|
|
87
|
+
return True
|
|
88
|
+
if isinstance(grade, str):
|
|
89
|
+
if grade.casefold() == "complete":
|
|
90
|
+
return True
|
|
91
|
+
# Numeric grades (points, percentages) count as passing
|
|
92
|
+
if (
|
|
93
|
+
grade.isdigit()
|
|
94
|
+
or grade.replace(".", "", 1).isdigit()
|
|
95
|
+
or grade.replace("%", "", 1).isdigit()
|
|
96
|
+
):
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def missing_assignments(
|
|
102
|
+
assignments_list, users_list, passing_regex=None, optional_assignments=None
|
|
103
|
+
):
|
|
104
|
+
"""
|
|
105
|
+
Returns missing assignments for conjunctive average with surveys.
|
|
106
|
+
|
|
107
|
+
Any assignment without a passing grade (A-E, P, complete, or numeric) is
|
|
108
|
+
missing.
|
|
109
|
+
"""
|
|
110
|
+
from canvaslms.cli import results
|
|
111
|
+
|
|
112
|
+
return results.missing_assignments(
|
|
113
|
+
assignments_list,
|
|
114
|
+
users_list,
|
|
115
|
+
optional_assignments=optional_assignments,
|
|
116
|
+
is_passing=is_passing_grade,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
80
120
|
def summarize_group(assignments_list, users_list):
|
|
81
121
|
"""
|
|
82
122
|
Summarizes a particular set of assignments (assignments_list) for all
|
canvaslms/grades/disjunctmax.nw
CHANGED
|
@@ -132,4 +132,63 @@ def int_to_grade(int_grade):
|
|
|
132
132
|
0: "P",
|
|
133
133
|
1: "E", 2: "D", 3: "C", 4: "B", 5: "A"}
|
|
134
134
|
return grade_map_inv[int_grade]
|
|
135
|
+
@
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
\subsection{Finding missing assignments}
|
|
139
|
+
|
|
140
|
+
For disjunctive maximum grading, the semantics of \enquote{missing} are
|
|
141
|
+
fundamentally different from conjunctive modules.
|
|
142
|
+
Since a student only needs ONE passing grade to pass the group:
|
|
143
|
+
\begin{itemize}
|
|
144
|
+
\item If the student has at least one passing grade, \emph{nothing} is missing.
|
|
145
|
+
\item If the student has no passing grades, we report \emph{all} assignments
|
|
146
|
+
as options they could complete to pass.
|
|
147
|
+
\end{itemize}
|
|
148
|
+
|
|
149
|
+
This is in contrast to conjunctive modules (like [[conjunctavg]]), where each
|
|
150
|
+
non-passing assignment is independently \enquote{missing}.
|
|
151
|
+
|
|
152
|
+
We cannot reuse the shared implementation from [[canvaslms.cli.results]] because
|
|
153
|
+
that iterates assignment-by-assignment.
|
|
154
|
+
Disjunctive logic requires evaluating ALL assignments first, then deciding what
|
|
155
|
+
to report.
|
|
156
|
+
<<helper functions>>=
|
|
157
|
+
def missing_assignments(assignments_list, users_list,
|
|
158
|
+
passing_regex=None,
|
|
159
|
+
optional_assignments=None):
|
|
160
|
+
"""
|
|
161
|
+
Returns missing assignments for disjunctive maximum grading.
|
|
162
|
+
|
|
163
|
+
Only reports assignments if the student has NO passing grades.
|
|
164
|
+
If they have at least one passing grade, nothing is missing.
|
|
165
|
+
"""
|
|
166
|
+
import re
|
|
167
|
+
|
|
168
|
+
for user in users_list:
|
|
169
|
+
grades_with_assignments = []
|
|
170
|
+
|
|
171
|
+
# First pass: collect all grades
|
|
172
|
+
for assignment in assignments_list:
|
|
173
|
+
# Skip optional assignments
|
|
174
|
+
if optional_assignments:
|
|
175
|
+
if any(re.search(opt, assignment.name) for opt in optional_assignments):
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
submission = assignment.get_submission(user)
|
|
180
|
+
grade = submission.grade or "F"
|
|
181
|
+
except ResourceDoesNotExist:
|
|
182
|
+
grade = "F"
|
|
183
|
+
|
|
184
|
+
grades_with_assignments.append((assignment, grade))
|
|
185
|
+
|
|
186
|
+
# Check if student has at least one passing grade (P or better)
|
|
187
|
+
has_passing = any(grade_to_int(g) >= 0 for _, g in grades_with_assignments)
|
|
188
|
+
|
|
189
|
+
if not has_passing:
|
|
190
|
+
# Report all assignments as options to complete
|
|
191
|
+
for assignment, grade in grades_with_assignments:
|
|
192
|
+
yield user, assignment, \
|
|
193
|
+
f"not a passing grade ({grade}) - complete any one to pass"
|
|
135
194
|
|
canvaslms/grades/disjunctmax.py
CHANGED
|
@@ -88,6 +88,44 @@ def int_to_grade(int_grade):
|
|
|
88
88
|
return grade_map_inv[int_grade]
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
def missing_assignments(
|
|
92
|
+
assignments_list, users_list, passing_regex=None, optional_assignments=None
|
|
93
|
+
):
|
|
94
|
+
"""
|
|
95
|
+
Returns missing assignments for disjunctive maximum grading.
|
|
96
|
+
|
|
97
|
+
Only reports assignments if the student has NO passing grades.
|
|
98
|
+
If they have at least one passing grade, nothing is missing.
|
|
99
|
+
"""
|
|
100
|
+
import re
|
|
101
|
+
|
|
102
|
+
for user in users_list:
|
|
103
|
+
grades_with_assignments = []
|
|
104
|
+
|
|
105
|
+
# First pass: collect all grades
|
|
106
|
+
for assignment in assignments_list:
|
|
107
|
+
# Skip optional assignments
|
|
108
|
+
if optional_assignments:
|
|
109
|
+
if any(re.search(opt, assignment.name) for opt in optional_assignments):
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
submission = assignment.get_submission(user)
|
|
114
|
+
grade = submission.grade or "F"
|
|
115
|
+
except ResourceDoesNotExist:
|
|
116
|
+
grade = "F"
|
|
117
|
+
|
|
118
|
+
grades_with_assignments.append((assignment, grade))
|
|
119
|
+
|
|
120
|
+
# Check if student has at least one passing grade (P or better)
|
|
121
|
+
has_passing = any(grade_to_int(g) >= 0 for _, g in grades_with_assignments)
|
|
122
|
+
|
|
123
|
+
if not has_passing:
|
|
124
|
+
# Report all assignments as options to complete
|
|
125
|
+
for assignment, grade in grades_with_assignments:
|
|
126
|
+
yield user, assignment, f"not a passing grade ({grade}) - complete any one to pass"
|
|
127
|
+
|
|
128
|
+
|
|
91
129
|
def summarize_group(assignments_list, users_list):
|
|
92
130
|
"""Summarizes a particular set of assignments (assignments_list) for all
|
|
93
131
|
users in users_list"""
|
canvaslms/grades/grades.nw
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
This is the documentation for the \texttt{canvaslms.grades} package.
|
|
5
5
|
<<module doc>>=
|
|
6
|
-
This package contains modules to summarize assignment groups in different ways.
|
|
6
|
+
This package contains modules to summarize assignment groups in different ways.
|
|
7
7
|
These modules are used with the `-S` option of the `results` command.
|
|
8
8
|
|
|
9
|
-
For a module to be used with the `canvaslms results -S module` option, the
|
|
9
|
+
For a module to be used with the `canvaslms results -S module` option, the
|
|
10
10
|
module must fulfil the following:
|
|
11
11
|
|
|
12
12
|
1) It must contain a function named `summarize_group`.
|
|
@@ -22,7 +22,21 @@ module must fulfil the following:
|
|
|
22
22
|
3) The return value should be a list of lists. Each list should have the
|
|
23
23
|
form `[user, grade, grade date, grader 1, ..., grader N]`.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
OPTIONAL: A module may also provide a `missing_assignments` function for use
|
|
26
|
+
with the `--missing` flag. If provided, it will be used instead of the default.
|
|
27
|
+
|
|
28
|
+
- `missing_assignments(assignments_list, users_list, passing_regex=...,
|
|
29
|
+
optional_assignments=...)` should yield `(user, assignment, reason)` tuples.
|
|
30
|
+
|
|
31
|
+
- What "missing" means depends on the grading policy:
|
|
32
|
+
* Conjunctive modules (all must pass): Any non-passing assignment is missing
|
|
33
|
+
* Disjunctive modules (at least one must pass): Only report if NO passing
|
|
34
|
+
|
|
35
|
+
- Modules can define `is_passing_grade(grade)` and call the shared default
|
|
36
|
+
implementation from `canvaslms.cli.results.missing_assignments()` with the
|
|
37
|
+
`is_passing` callback parameter.
|
|
38
|
+
|
|
39
|
+
For more details, see Chapter 11 of the `canvaslms.pdf` file found among the
|
|
26
40
|
release files at:
|
|
27
41
|
|
|
28
42
|
https://github.com/dbosk/canvaslms/releases
|
|
@@ -101,4 +101,62 @@ def summarize(user, assignments_list):
|
|
|
101
101
|
final_grade = None
|
|
102
102
|
|
|
103
103
|
return (final_grade, final_date, graders)
|
|
104
|
+
@
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
\subsection{Finding missing assignments}
|
|
108
|
+
|
|
109
|
+
Like [[disjunctmax]], this module uses disjunctive grading: a student only needs
|
|
110
|
+
ONE passing grade.
|
|
111
|
+
The semantics of \enquote{missing} are the same:
|
|
112
|
+
\begin{itemize}
|
|
113
|
+
\item If the student has at least one passing grade, nothing is missing.
|
|
114
|
+
\item If the student has no passing grades, all assignments are reported as
|
|
115
|
+
options.
|
|
116
|
+
\end{itemize}
|
|
117
|
+
|
|
118
|
+
The key difference from [[disjunctmax]] is that unknown grades (grades not in
|
|
119
|
+
A--F or P) are treated as F.
|
|
120
|
+
This accommodates surveys where numeric scores don't count as grades.
|
|
121
|
+
<<helper functions>>=
|
|
122
|
+
def missing_assignments(assignments_list, users_list,
|
|
123
|
+
passing_regex=None,
|
|
124
|
+
optional_assignments=None):
|
|
125
|
+
"""
|
|
126
|
+
Returns missing assignments for disjunctive maximum grading with surveys.
|
|
127
|
+
|
|
128
|
+
Only reports assignments if the student has NO passing grades.
|
|
129
|
+
Unknown grades (not A-F or P) are treated as F.
|
|
130
|
+
"""
|
|
131
|
+
import re
|
|
132
|
+
|
|
133
|
+
for user in users_list:
|
|
134
|
+
grades_with_assignments = []
|
|
135
|
+
|
|
136
|
+
# First pass: collect all grades
|
|
137
|
+
for assignment in assignments_list:
|
|
138
|
+
# Skip optional assignments
|
|
139
|
+
if optional_assignments:
|
|
140
|
+
if any(re.search(opt, assignment.name) for opt in optional_assignments):
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
submission = assignment.get_submission(user)
|
|
145
|
+
grade = submission.grade
|
|
146
|
+
# Unknown grades are treated as F
|
|
147
|
+
if grade is None or grade not in "ABCDEPF":
|
|
148
|
+
grade = "F"
|
|
149
|
+
except ResourceDoesNotExist:
|
|
150
|
+
grade = "F"
|
|
151
|
+
|
|
152
|
+
grades_with_assignments.append((assignment, grade))
|
|
153
|
+
|
|
154
|
+
# Check if student has at least one passing grade (P or better)
|
|
155
|
+
has_passing = any(grade_max([g]) != "F" for _, g in grades_with_assignments)
|
|
156
|
+
|
|
157
|
+
if not has_passing:
|
|
158
|
+
# Report all assignments as options to complete
|
|
159
|
+
for assignment, grade in grades_with_assignments:
|
|
160
|
+
yield user, assignment, \
|
|
161
|
+
f"not a passing grade ({grade}) - complete any one to pass"
|
|
104
162
|
|
|
@@ -60,6 +60,47 @@ def summarize(user, assignments_list):
|
|
|
60
60
|
return (final_grade, final_date, graders)
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def missing_assignments(
|
|
64
|
+
assignments_list, users_list, passing_regex=None, optional_assignments=None
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Returns missing assignments for disjunctive maximum grading with surveys.
|
|
68
|
+
|
|
69
|
+
Only reports assignments if the student has NO passing grades.
|
|
70
|
+
Unknown grades (not A-F or P) are treated as F.
|
|
71
|
+
"""
|
|
72
|
+
import re
|
|
73
|
+
|
|
74
|
+
for user in users_list:
|
|
75
|
+
grades_with_assignments = []
|
|
76
|
+
|
|
77
|
+
# First pass: collect all grades
|
|
78
|
+
for assignment in assignments_list:
|
|
79
|
+
# Skip optional assignments
|
|
80
|
+
if optional_assignments:
|
|
81
|
+
if any(re.search(opt, assignment.name) for opt in optional_assignments):
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
submission = assignment.get_submission(user)
|
|
86
|
+
grade = submission.grade
|
|
87
|
+
# Unknown grades are treated as F
|
|
88
|
+
if grade is None or grade not in "ABCDEPF":
|
|
89
|
+
grade = "F"
|
|
90
|
+
except ResourceDoesNotExist:
|
|
91
|
+
grade = "F"
|
|
92
|
+
|
|
93
|
+
grades_with_assignments.append((assignment, grade))
|
|
94
|
+
|
|
95
|
+
# Check if student has at least one passing grade (P or better)
|
|
96
|
+
has_passing = any(grade_max([g]) != "F" for _, g in grades_with_assignments)
|
|
97
|
+
|
|
98
|
+
if not has_passing:
|
|
99
|
+
# Report all assignments as options to complete
|
|
100
|
+
for assignment, grade in grades_with_assignments:
|
|
101
|
+
yield user, assignment, f"not a passing grade ({grade}) - complete any one to pass"
|
|
102
|
+
|
|
103
|
+
|
|
63
104
|
def summarize_group(assignments_list, users_list):
|
|
64
105
|
"""Summarizes a particular set of assignments (assignments_list) for all
|
|
65
106
|
users in users_list"""
|
|
@@ -216,3 +216,30 @@ else:
|
|
|
216
216
|
final_date = None
|
|
217
217
|
final_grade = None
|
|
218
218
|
@
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
\subsection{Finding missing assignments}
|
|
222
|
+
|
|
223
|
+
For participation grading, a student is \enquote{missing} any assignment that
|
|
224
|
+
doesn't have a passing grade ([[complete]] or [[100]]).
|
|
225
|
+
Since this is conjunctive grading (all must pass), each incomplete assignment
|
|
226
|
+
prevents the student from passing the group.
|
|
227
|
+
|
|
228
|
+
We already have [[is_passing_grade]] defined above, so we simply reuse the
|
|
229
|
+
shared implementation from [[canvaslms.cli.results]] with our callback.
|
|
230
|
+
<<helper functions>>=
|
|
231
|
+
def missing_assignments(assignments_list, users_list,
|
|
232
|
+
passing_regex=None,
|
|
233
|
+
optional_assignments=None):
|
|
234
|
+
"""
|
|
235
|
+
Returns missing assignments for participation grading.
|
|
236
|
+
|
|
237
|
+
Any assignment without 'complete' or '100' is missing.
|
|
238
|
+
"""
|
|
239
|
+
from canvaslms.cli import results
|
|
240
|
+
return results.missing_assignments(
|
|
241
|
+
assignments_list, users_list,
|
|
242
|
+
optional_assignments=optional_assignments,
|
|
243
|
+
is_passing=is_passing_grade
|
|
244
|
+
)
|
|
245
|
+
@
|
|
@@ -60,6 +60,24 @@ def summarize(user, assignments_list):
|
|
|
60
60
|
return (final_grade, final_date, graders)
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def missing_assignments(
|
|
64
|
+
assignments_list, users_list, passing_regex=None, optional_assignments=None
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Returns missing assignments for participation grading.
|
|
68
|
+
|
|
69
|
+
Any assignment without 'complete' or '100' is missing.
|
|
70
|
+
"""
|
|
71
|
+
from canvaslms.cli import results
|
|
72
|
+
|
|
73
|
+
return results.missing_assignments(
|
|
74
|
+
assignments_list,
|
|
75
|
+
users_list,
|
|
76
|
+
optional_assignments=optional_assignments,
|
|
77
|
+
is_passing=is_passing_grade,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
63
81
|
def summarize_group(assignments_list, users_list):
|
|
64
82
|
"""
|
|
65
83
|
Summarizes participation assignments using conjunctive P/F grading.
|