ladok3 4.10__py3-none-any.whl → 5.4__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.
- doc/ltxobj/ladok3.pdf +0 -0
- ladok3/Makefile +6 -0
- ladok3/__init__.py +1511 -3386
- ladok3/api.nw +1653 -225
- ladok3/cli.nw +118 -53
- ladok3/cli.py +323 -252
- ladok3/data.nw +92 -15
- ladok3/data.py +79 -3
- ladok3/ladok.bash +35 -17
- ladok3/ladok3.nw +288 -16
- ladok3/report.nw +183 -117
- ladok3/report.py +135 -63
- ladok3/scripts.nw +244 -0
- ladok3/student.nw +69 -4
- ladok3/student.py +98 -42
- ladok3/undoc.nw +62 -3119
- {ladok3-4.10.dist-info → ladok3-5.4.dist-info}/LICENSE +1 -1
- {ladok3-4.10.dist-info → ladok3-5.4.dist-info}/METADATA +39 -17
- ladok3-5.4.dist-info/RECORD +21 -0
- {ladok3-4.10.dist-info → ladok3-5.4.dist-info}/WHEEL +1 -1
- ladok3/.gitignore +0 -10
- ladok3-4.10.dist-info/RECORD +0 -21
- {ladok3-4.10.dist-info → ladok3-5.4.dist-info}/entry_points.txt +0 -0
ladok3/report.nw
CHANGED
|
@@ -32,6 +32,14 @@ import sys
|
|
|
32
32
|
<<functions>>
|
|
33
33
|
|
|
34
34
|
def add_command_options(parser):
|
|
35
|
+
"""Add the 'report' subcommand options to the argument parser.
|
|
36
|
+
|
|
37
|
+
Creates a subparser for result reporting with options for single results
|
|
38
|
+
via command line arguments or batch processing via CSV input.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
parser (ArgumentParser): The parent parser to add the subcommand to.
|
|
42
|
+
"""
|
|
35
43
|
report_parser = parser.add_parser("report",
|
|
36
44
|
help="Reports course results to LADOK",
|
|
37
45
|
description="Reports course results to LADOK"
|
|
@@ -40,6 +48,12 @@ def add_command_options(parser):
|
|
|
40
48
|
<<add report command arguments to report parser>>
|
|
41
49
|
|
|
42
50
|
def command(ladok, args):
|
|
51
|
+
"""Execute the result reporting command.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
ladok (LadokSession): The LADOK session for data access.
|
|
55
|
+
args: Parsed command line arguments containing result data or CSV options.
|
|
56
|
+
"""
|
|
43
57
|
<<report results depending on args>>
|
|
44
58
|
@
|
|
45
59
|
|
|
@@ -60,9 +74,21 @@ many_parser = report_parser.add_argument_group(
|
|
|
60
74
|
We provide two functions, one for each way of reporting the results.
|
|
61
75
|
<<functions>>=
|
|
62
76
|
def report_one_result(ladok, args):
|
|
77
|
+
"""Report a single result specified via command line arguments.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
ladok (LadokSession): The LADOK session for data access.
|
|
81
|
+
args: Command line arguments containing course, student, component, and grade info.
|
|
82
|
+
"""
|
|
63
83
|
<<report results given in args>>
|
|
64
84
|
|
|
65
85
|
def report_many_results(ladok, args):
|
|
86
|
+
"""Report multiple results read from CSV data on stdin.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
ladok (LadokSession): The LADOK session for data access.
|
|
90
|
+
args: Command line arguments containing CSV processing options.
|
|
91
|
+
"""
|
|
66
92
|
<<report results given in stdin>>
|
|
67
93
|
@
|
|
68
94
|
|
|
@@ -83,6 +109,141 @@ report_parser.add_argument("-f", "--finalize",
|
|
|
83
109
|
@
|
|
84
110
|
|
|
85
111
|
|
|
112
|
+
\section{Report many results given in standard input}
|
|
113
|
+
|
|
114
|
+
We want to read CSV data from standard input.
|
|
115
|
+
<<report results given in stdin>>=
|
|
116
|
+
data_reader = csv.reader(sys.stdin, delimiter=args.delimiter)
|
|
117
|
+
for course_code, component_code, student_id, grade, date, *graders in data_reader:
|
|
118
|
+
<<report a result read from stdin>>
|
|
119
|
+
@ We need to add an argument for the delimiter.
|
|
120
|
+
<<add many results group arguments>>=
|
|
121
|
+
many_parser.add_argument("-d", "--delimiter",
|
|
122
|
+
default="\t",
|
|
123
|
+
help="The delimiter for the CSV input; "
|
|
124
|
+
"default is a tab character to be compatible with POSIX commands, "
|
|
125
|
+
"use `-d,` or `-d ,` to get comma-separated values.")
|
|
126
|
+
@
|
|
127
|
+
|
|
128
|
+
We also want to handle errors and confirmations.
|
|
129
|
+
When reporting in bulk, we don't want unnecessary errors.
|
|
130
|
+
We also want to have a summary of the changes.
|
|
131
|
+
<<add many results group arguments>>=
|
|
132
|
+
many_parser.add_argument("-v", "--verbose",
|
|
133
|
+
action="count", default=0,
|
|
134
|
+
help="Increases the verbosity of the output: -v will print results that "
|
|
135
|
+
"were reported to standard out. Otherwise only errors are printed.")
|
|
136
|
+
@
|
|
137
|
+
|
|
138
|
+
Then we can actually report the result using the values read from [[stdin]].
|
|
139
|
+
<<report a result read from stdin>>=
|
|
140
|
+
try:
|
|
141
|
+
set_grade(ladok, args,
|
|
142
|
+
student_id, course_code, component_code, grade, date, graders)
|
|
143
|
+
except Exception as err:
|
|
144
|
+
<<try to resolve [[student]] from [[ladok]] using [[student_id]]>>
|
|
145
|
+
print(f"{course_code} {component_code}={grade} ({date}) {student}: "
|
|
146
|
+
f"{err}",
|
|
147
|
+
file=sys.stderr)
|
|
148
|
+
@
|
|
149
|
+
|
|
150
|
+
The reason we want to resolve the student from LADOK is that the [[student_id]]
|
|
151
|
+
might be a personnummer or a LADOK ID---if the latter, it's not particularly
|
|
152
|
+
readable for a human and we can't use the LADOK ID in the LADOK web interface
|
|
153
|
+
when we want to deal with the errors manually.
|
|
154
|
+
But if we resolve the student, then we get a readable name.
|
|
155
|
+
<<try to resolve [[student]] from [[ladok]] using [[student_id]]>>=
|
|
156
|
+
try:
|
|
157
|
+
student = ladok.get_student(student_id)
|
|
158
|
+
except Exception:
|
|
159
|
+
student = student_id
|
|
160
|
+
@
|
|
161
|
+
|
|
162
|
+
When we set the grade, there are a few cases that should be handled.
|
|
163
|
+
If the grade isn't attested, we try to change it.
|
|
164
|
+
(This might still fail if the grade is finalized but not attested.)
|
|
165
|
+
If we've selected the verbose option, then we print what we have reported.
|
|
166
|
+
|
|
167
|
+
If the grade was attested, then we check if it's different.
|
|
168
|
+
If it's different, we output this.
|
|
169
|
+
If it's the same, we silently ignore it.
|
|
170
|
+
This is best for bulk reporting, because then we can always try to report for
|
|
171
|
+
all students.
|
|
172
|
+
|
|
173
|
+
We want to report errors as exceptions.
|
|
174
|
+
<<functions>>=
|
|
175
|
+
def set_grade(ladok, args,
|
|
176
|
+
student_id, course_code, component_code, grade, date, graders):
|
|
177
|
+
"""Set a grade for a student's course component result.
|
|
178
|
+
|
|
179
|
+
Handles the logic for setting grades, including checking if results are
|
|
180
|
+
already attested, validating dates, and optionally finalizing results.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
ladok (LadokSession): The LADOK session for data access.
|
|
184
|
+
args: Command line arguments with options like verbose and finalize.
|
|
185
|
+
student_id (str): Student identifier (personnummer or LADOK ID).
|
|
186
|
+
course_code (str): Course code (e.g., "DD1315").
|
|
187
|
+
component_code (str): Component code (e.g., "LAB1").
|
|
188
|
+
grade (str): Grade to assign (e.g., "P", "F", "A").
|
|
189
|
+
date (str): Examination date in YYYY-MM-DD format.
|
|
190
|
+
graders (list): List of grader identifiers.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
Exception: If the grade setting fails or validation errors occur.
|
|
194
|
+
"""
|
|
195
|
+
student = ladok.get_student(student_id)
|
|
196
|
+
<<get [[course]] from [[student]] and [[course_code]]>>
|
|
197
|
+
<<get [[component]] from [[course]] and [[component_code]]>>
|
|
198
|
+
|
|
199
|
+
if not component.attested and component.grade != grade:
|
|
200
|
+
<<ensure [[date]] is a valid date for [[course]]>>
|
|
201
|
+
component.set_grade(grade, date)
|
|
202
|
+
if args.finalize:
|
|
203
|
+
component.finalize(graders)
|
|
204
|
+
if args.verbose:
|
|
205
|
+
print(f"{course_code} {student}: reported "
|
|
206
|
+
f"{component.component} = {component.grade} ({date}) "
|
|
207
|
+
f"by {', '.join(graders)}.")
|
|
208
|
+
elif component.grade != grade:
|
|
209
|
+
raise LadokValidationError(f"attested {component.component} "
|
|
210
|
+
f"result {component.grade} ({component.date}) "
|
|
211
|
+
f"is different from {grade} ({date}).")
|
|
212
|
+
@
|
|
213
|
+
|
|
214
|
+
Now we simply want to set those objects up.
|
|
215
|
+
We want to throw exceptions that explain what the problem is if these don't
|
|
216
|
+
exist.
|
|
217
|
+
<<get [[course]] from [[student]] and [[course_code]]>>=
|
|
218
|
+
try:
|
|
219
|
+
course = student.courses(code=course_code)[0]
|
|
220
|
+
except IndexError:
|
|
221
|
+
raise LadokNotFoundError(f"{course_code}: No such course for {student}")
|
|
222
|
+
<<get [[component]] from [[course]] and [[component_code]]>>=
|
|
223
|
+
try:
|
|
224
|
+
component = course.results(component=component_code)[0]
|
|
225
|
+
except IndexError:
|
|
226
|
+
raise LadokNotFoundError(f"{component_code}: no such component for {course_code}")
|
|
227
|
+
@
|
|
228
|
+
|
|
229
|
+
Finally, we want to ensure the date is correct.
|
|
230
|
+
The date must be at the earliest the start of the course.
|
|
231
|
+
The student can't finish any results before the course has started.
|
|
232
|
+
LADOK will not accept that.
|
|
233
|
+
<<ensure [[date]] is a valid date for [[course]]>>=
|
|
234
|
+
if not isinstance(date, datetime.date):
|
|
235
|
+
date = datetime.date.fromisoformat(date)
|
|
236
|
+
|
|
237
|
+
if date < course.start:
|
|
238
|
+
print(f"{course_code} {component_code}={grade} "
|
|
239
|
+
f"({date}) {student}: "
|
|
240
|
+
f"Grade date ({date}) is before "
|
|
241
|
+
f"course start date ({course.start}), "
|
|
242
|
+
f"using course start date instead.")
|
|
243
|
+
date = course.start
|
|
244
|
+
@
|
|
245
|
+
|
|
246
|
+
|
|
86
247
|
\section{Report a result given on command line}
|
|
87
248
|
|
|
88
249
|
If we've chosen to give one result on the command line, then we'll need the
|
|
@@ -91,28 +252,33 @@ following arguments.
|
|
|
91
252
|
We start with the course, component code, the student's ID and grade.
|
|
92
253
|
<<add one result group arguments>>=
|
|
93
254
|
one_parser.add_argument("course_code", nargs="?",
|
|
94
|
-
help="The course code (e.g. DD1315) for which the grade is for"
|
|
255
|
+
help="The course code (e.g. DD1315) for which the grade is for."
|
|
95
256
|
)
|
|
96
257
|
|
|
97
258
|
one_parser.add_argument("component_code", nargs="?",
|
|
98
|
-
help="The component code (e.g. LAB1) for which the grade is for"
|
|
259
|
+
help="The component code (e.g. LAB1) for which the grade is for. "
|
|
260
|
+
"This can be set to the course code (e.g. DD1315) to set the "
|
|
261
|
+
"final grade for the course. But all components must be "
|
|
262
|
+
"certified (attested) before the course grade can be set."
|
|
99
263
|
)
|
|
100
264
|
|
|
101
265
|
one_parser.add_argument("student_id", nargs="?",
|
|
102
|
-
help="Student identifier (personnummer or LADOK ID)"
|
|
266
|
+
help="Student identifier (personnummer or LADOK ID)."
|
|
103
267
|
)
|
|
104
268
|
|
|
105
269
|
one_parser.add_argument("grade", nargs="?",
|
|
106
|
-
help="The grade (e.g. A or P)"
|
|
270
|
+
help="The grade (e.g. A or P)."
|
|
107
271
|
)
|
|
108
|
-
@ We must make them optional like this to make it work with
|
|
109
|
-
alternative, so we must check ourselves that
|
|
272
|
+
@ We must make them optional like this to make it work with our second
|
|
273
|
+
alternative (bulk reporting through [[stdin]]), so we must check ourselves that
|
|
274
|
+
we got the arguments.
|
|
110
275
|
<<check that we got all positional arguments>>=
|
|
111
276
|
if not (args.course_code and args.component_code and
|
|
112
277
|
args.student_id and args.grade):
|
|
113
278
|
print(f"{sys.argv[0]} report: "
|
|
114
|
-
|
|
115
|
-
|
|
279
|
+
"not all positional args given: "
|
|
280
|
+
"course_code, component, student, grade",
|
|
281
|
+
file=sys.stderr)
|
|
116
282
|
sys.exit(1)
|
|
117
283
|
@
|
|
118
284
|
|
|
@@ -123,7 +289,7 @@ If it's not provided, we let [[argparse]] set it to today's date.
|
|
|
123
289
|
<<add one result group arguments>>=
|
|
124
290
|
one_parser.add_argument("date", nargs="?",
|
|
125
291
|
help="Date on ISO format (e.g. 2021-03-18), "
|
|
126
|
-
|
|
292
|
+
f"defaults to today's date ({datetime.date.today()}).",
|
|
127
293
|
type=datetime.date.fromisoformat,
|
|
128
294
|
default=datetime.date.today()
|
|
129
295
|
)
|
|
@@ -141,115 +307,15 @@ them.
|
|
|
141
307
|
<<report results given in args>>=
|
|
142
308
|
<<check that we got all positional arguments>>
|
|
143
309
|
try:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
result.set_grade(args.grade, args.date)
|
|
148
|
-
if args.finalize:
|
|
149
|
-
result.finalize(args.graders)
|
|
310
|
+
set_grade(ladok, args,
|
|
311
|
+
args.student_id, args.course_code, args.component_code,
|
|
312
|
+
args.grade, args.date, args.graders)
|
|
150
313
|
except Exception as err:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
that one here.
|
|
157
|
-
|
|
158
|
-
Both looking up the course and the component can yield index errors.
|
|
159
|
-
We'd like to distinguish these.
|
|
160
|
-
We will catch the exception, the reraise the same exception but with a better
|
|
161
|
-
error message.
|
|
162
|
-
<<look up [[course]] from [[student]]>>=
|
|
163
|
-
try:
|
|
164
|
-
course = student.courses(code=args.course_code)[0]
|
|
165
|
-
except IndexError:
|
|
166
|
-
raise Exception("f{args.course_code}: No such course for {student}")
|
|
167
|
-
<<look up [[result]] from [[course]]>>=
|
|
168
|
-
try:
|
|
169
|
-
result = course.results(component=args.component_code)[0]
|
|
170
|
-
except IndexError:
|
|
171
|
-
raise Exception(f"{args.component_code}: "
|
|
172
|
-
f"No such component for {args.course_code}")
|
|
173
|
-
@
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
\section{Report many results given in standard input}
|
|
177
|
-
|
|
178
|
-
We want to read CSV data from standard input.
|
|
179
|
-
<<report results given in stdin>>=
|
|
180
|
-
data_reader = csv.reader(sys.stdin, delimiter=args.delimiter)
|
|
181
|
-
for course_code, component_code, student_id, grade, date, *graders in data_reader:
|
|
182
|
-
<<report a result read from stdin>>
|
|
183
|
-
@ We need to add an argument for the delimiter.
|
|
184
|
-
<<add many results group arguments>>=
|
|
185
|
-
many_parser.add_argument("-d", "--delimiter",
|
|
186
|
-
default="\t",
|
|
187
|
-
help="The delimiter for the CSV input; "
|
|
188
|
-
"default is a tab character to be compatible with POSIX commands, "
|
|
189
|
-
"use `-d,` or `-d ,` to get comma-separated values.")
|
|
190
|
-
@
|
|
191
|
-
|
|
192
|
-
We also want to handle errors and confirmations differently.
|
|
193
|
-
When reporting in bulk, we don't want unnecessary errors.
|
|
194
|
-
We also want to have a summary of the changes.
|
|
195
|
-
<<add many results group arguments>>=
|
|
196
|
-
many_parser.add_argument("-v", "--verbose",
|
|
197
|
-
action="count", default=0,
|
|
198
|
-
help="Increases the verbosity of the output: -v will print results that "
|
|
199
|
-
"were reported to standard out. Otherwise only errors are printed.")
|
|
200
|
-
@
|
|
201
|
-
|
|
202
|
-
When we actually report a result, this is similar to how we did it above.
|
|
203
|
-
The difference is that we've read the variables from the CSV data in [[stdin]]
|
|
204
|
-
instead of from [[args]].
|
|
205
|
-
<<report a result read from stdin>>=
|
|
206
|
-
try:
|
|
207
|
-
student = ladok.get_student(student_id)
|
|
208
|
-
|
|
209
|
-
try:
|
|
210
|
-
course = student.courses(code=course_code)[0]
|
|
211
|
-
except IndexError:
|
|
212
|
-
raise Exception(f"{course_code}: No such course for {student}")
|
|
213
|
-
|
|
214
|
-
try:
|
|
215
|
-
component = course.results(component=component_code)[0]
|
|
216
|
-
except IndexError:
|
|
217
|
-
raise Exception(f"{component_code}: no such component for {course_code}")
|
|
218
|
-
|
|
219
|
-
<<set [[grade]] to [[component]], output if verbose>>
|
|
220
|
-
except Exception as err:
|
|
221
|
-
try:
|
|
222
|
-
print(f"{course_code} {component_code}={grade} ({date}) {student}: {err}",
|
|
223
|
-
file=sys.stderr)
|
|
224
|
-
except ValueError as verr:
|
|
225
|
-
print(f"{verr}: "
|
|
226
|
-
f"{course_code} {component_code}={grade} ({date}) {student_id}: {err}",
|
|
227
|
-
file=sys.stderr)
|
|
228
|
-
@
|
|
229
|
-
|
|
230
|
-
Now, when we set the grade, there are a few cases that should be handled.
|
|
231
|
-
If the grade isn't attested, we try to change it.
|
|
232
|
-
(This might still fail if the grade is finalized but not attested.)
|
|
233
|
-
If we've selected the verbose option, then we print what we have reported.
|
|
234
|
-
|
|
235
|
-
If the grade was attested, then we check if it's different.
|
|
236
|
-
If it's different, we output this.
|
|
237
|
-
If it's the same, we silently ignore it.
|
|
238
|
-
This is best for bulk reporting, because then we can always try to report for
|
|
239
|
-
all students.
|
|
240
|
-
<<set [[grade]] to [[component]], output if verbose>>=
|
|
241
|
-
if not component.attested and component.grade != grade:
|
|
242
|
-
component.set_grade(grade, date)
|
|
243
|
-
if args.finalize:
|
|
244
|
-
component.finalize(graders)
|
|
245
|
-
if args.verbose:
|
|
246
|
-
print(f"{course_code} {student}: reported "
|
|
247
|
-
f"{component.component} = {component.grade} ({date}) "
|
|
248
|
-
f"by {', '.join(graders)}.")
|
|
249
|
-
elif component.grade != grade:
|
|
250
|
-
print(f"{course_code} {student}: attested {component.component} "
|
|
251
|
-
f"result {component.grade} ({component.date}) "
|
|
252
|
-
f"is different from {grade} ({date}).")
|
|
314
|
+
student_id = args.student_id
|
|
315
|
+
<<try to resolve [[student]] from [[ladok]] using [[student_id]]>>
|
|
316
|
+
print(f"{args.course_code} {args.component_code}={args.grade} ({args.date}) "
|
|
317
|
+
f"{student}: {err}",
|
|
318
|
+
file=sys.stderr)
|
|
253
319
|
@
|
|
254
320
|
|
|
255
321
|
|
ladok3/report.py
CHANGED
|
@@ -5,86 +5,149 @@ import sys
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def report_one_result(ladok, args):
|
|
8
|
+
"""Report a single result specified via command line arguments.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
ladok (LadokSession): The LADOK session for data access.
|
|
12
|
+
args: Command line arguments containing course, student, component, and grade info.
|
|
13
|
+
"""
|
|
8
14
|
if not (
|
|
9
15
|
args.course_code and args.component_code and args.student_id and args.grade
|
|
10
16
|
):
|
|
11
17
|
print(
|
|
12
18
|
f"{sys.argv[0]} report: "
|
|
13
|
-
"not all positional args given:
|
|
19
|
+
"not all positional args given: "
|
|
20
|
+
"course_code, component, student, grade",
|
|
14
21
|
file=sys.stderr,
|
|
15
22
|
)
|
|
16
23
|
sys.exit(1)
|
|
17
24
|
try:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
result.set_grade(args.grade, args.date)
|
|
30
|
-
if args.finalize:
|
|
31
|
-
result.finalize(args.graders)
|
|
25
|
+
set_grade(
|
|
26
|
+
ladok,
|
|
27
|
+
args,
|
|
28
|
+
args.student_id,
|
|
29
|
+
args.course_code,
|
|
30
|
+
args.component_code,
|
|
31
|
+
args.grade,
|
|
32
|
+
args.date,
|
|
33
|
+
args.graders,
|
|
34
|
+
)
|
|
32
35
|
except Exception as err:
|
|
36
|
+
student_id = args.student_id
|
|
33
37
|
try:
|
|
34
|
-
|
|
35
|
-
except
|
|
36
|
-
|
|
38
|
+
student = ladok.get_student(student_id)
|
|
39
|
+
except Exception:
|
|
40
|
+
student = student_id
|
|
41
|
+
print(
|
|
42
|
+
f"{args.course_code} {args.component_code}={args.grade} ({args.date}) "
|
|
43
|
+
f"{student}: {err}",
|
|
44
|
+
file=sys.stderr,
|
|
45
|
+
)
|
|
37
46
|
|
|
38
47
|
|
|
39
48
|
def report_many_results(ladok, args):
|
|
49
|
+
"""Report multiple results read from CSV data on stdin.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
ladok (LadokSession): The LADOK session for data access.
|
|
53
|
+
args: Command line arguments containing CSV processing options.
|
|
54
|
+
"""
|
|
40
55
|
data_reader = csv.reader(sys.stdin, delimiter=args.delimiter)
|
|
41
56
|
for course_code, component_code, student_id, grade, date, *graders in data_reader:
|
|
42
57
|
try:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
raise Exception(
|
|
54
|
-
f"{component_code}: no such component for {course_code}"
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
if not component.attested and component.grade != grade:
|
|
58
|
-
component.set_grade(grade, date)
|
|
59
|
-
if args.finalize:
|
|
60
|
-
component.finalize(graders)
|
|
61
|
-
if args.verbose:
|
|
62
|
-
print(
|
|
63
|
-
f"{course_code} {student}: reported "
|
|
64
|
-
f"{component.component} = {component.grade} ({date}) "
|
|
65
|
-
f"by {', '.join(graders)}."
|
|
66
|
-
)
|
|
67
|
-
elif component.grade != grade:
|
|
68
|
-
print(
|
|
69
|
-
f"{course_code} {student}: attested {component.component} "
|
|
70
|
-
f"result {component.grade} ({component.date}) "
|
|
71
|
-
f"is different from {grade} ({date})."
|
|
72
|
-
)
|
|
58
|
+
set_grade(
|
|
59
|
+
ladok,
|
|
60
|
+
args,
|
|
61
|
+
student_id,
|
|
62
|
+
course_code,
|
|
63
|
+
component_code,
|
|
64
|
+
grade,
|
|
65
|
+
date,
|
|
66
|
+
graders,
|
|
67
|
+
)
|
|
73
68
|
except Exception as err:
|
|
74
69
|
try:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
70
|
+
student = ladok.get_student(student_id)
|
|
71
|
+
except Exception:
|
|
72
|
+
student = student_id
|
|
73
|
+
print(
|
|
74
|
+
f"{course_code} {component_code}={grade} ({date}) {student}: " f"{err}",
|
|
75
|
+
file=sys.stderr,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def set_grade(
|
|
80
|
+
ladok, args, student_id, course_code, component_code, grade, date, graders
|
|
81
|
+
):
|
|
82
|
+
"""Set a grade for a student's course component result.
|
|
83
|
+
|
|
84
|
+
Handles the logic for setting grades, including checking if results are
|
|
85
|
+
already attested, validating dates, and optionally finalizing results.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
ladok (LadokSession): The LADOK session for data access.
|
|
89
|
+
args: Command line arguments with options like verbose and finalize.
|
|
90
|
+
student_id (str): Student identifier (personnummer or LADOK ID).
|
|
91
|
+
course_code (str): Course code (e.g., "DD1315").
|
|
92
|
+
component_code (str): Component code (e.g., "LAB1").
|
|
93
|
+
grade (str): Grade to assign (e.g., "P", "F", "A").
|
|
94
|
+
date (str): Examination date in YYYY-MM-DD format.
|
|
95
|
+
graders (list): List of grader identifiers.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
Exception: If the grade setting fails or validation errors occur.
|
|
99
|
+
"""
|
|
100
|
+
student = ladok.get_student(student_id)
|
|
101
|
+
try:
|
|
102
|
+
course = student.courses(code=course_code)[0]
|
|
103
|
+
except IndexError:
|
|
104
|
+
raise LadokNotFoundError(f"{course_code}: No such course for {student}")
|
|
105
|
+
try:
|
|
106
|
+
component = course.results(component=component_code)[0]
|
|
107
|
+
except IndexError:
|
|
108
|
+
raise LadokNotFoundError(
|
|
109
|
+
f"{component_code}: no such component for {course_code}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if not component.attested and component.grade != grade:
|
|
113
|
+
if not isinstance(date, datetime.date):
|
|
114
|
+
date = datetime.date.fromisoformat(date)
|
|
115
|
+
|
|
116
|
+
if date < course.start:
|
|
117
|
+
print(
|
|
118
|
+
f"{course_code} {component_code}={grade} "
|
|
119
|
+
f"({date}) {student}: "
|
|
120
|
+
f"Grade date ({date}) is before "
|
|
121
|
+
f"course start date ({course.start}), "
|
|
122
|
+
f"using course start date instead."
|
|
123
|
+
)
|
|
124
|
+
date = course.start
|
|
125
|
+
component.set_grade(grade, date)
|
|
126
|
+
if args.finalize:
|
|
127
|
+
component.finalize(graders)
|
|
128
|
+
if args.verbose:
|
|
129
|
+
print(
|
|
130
|
+
f"{course_code} {student}: reported "
|
|
131
|
+
f"{component.component} = {component.grade} ({date}) "
|
|
132
|
+
f"by {', '.join(graders)}."
|
|
133
|
+
)
|
|
134
|
+
elif component.grade != grade:
|
|
135
|
+
raise LadokValidationError(
|
|
136
|
+
f"attested {component.component} "
|
|
137
|
+
f"result {component.grade} ({component.date}) "
|
|
138
|
+
f"is different from {grade} ({date})."
|
|
139
|
+
)
|
|
85
140
|
|
|
86
141
|
|
|
87
142
|
def add_command_options(parser):
|
|
143
|
+
"""Add the 'report' subcommand options to the argument parser.
|
|
144
|
+
|
|
145
|
+
Creates a subparser for result reporting with options for single results
|
|
146
|
+
via command line arguments or batch processing via CSV input.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
parser (ArgumentParser): The parent parser to add the subcommand to.
|
|
150
|
+
"""
|
|
88
151
|
report_parser = parser.add_parser(
|
|
89
152
|
"report",
|
|
90
153
|
help="Reports course results to LADOK",
|
|
@@ -97,25 +160,28 @@ def add_command_options(parser):
|
|
|
97
160
|
one_parser.add_argument(
|
|
98
161
|
"course_code",
|
|
99
162
|
nargs="?",
|
|
100
|
-
help="The course code (e.g. DD1315) for which the grade is for",
|
|
163
|
+
help="The course code (e.g. DD1315) for which the grade is for.",
|
|
101
164
|
)
|
|
102
165
|
|
|
103
166
|
one_parser.add_argument(
|
|
104
167
|
"component_code",
|
|
105
168
|
nargs="?",
|
|
106
|
-
help="The component code (e.g. LAB1) for which the grade is for"
|
|
169
|
+
help="The component code (e.g. LAB1) for which the grade is for. "
|
|
170
|
+
"This can be set to the course code (e.g. DD1315) to set the "
|
|
171
|
+
"final grade for the course. But all components must be "
|
|
172
|
+
"certified (attested) before the course grade can be set.",
|
|
107
173
|
)
|
|
108
174
|
|
|
109
175
|
one_parser.add_argument(
|
|
110
|
-
"student_id", nargs="?", help="Student identifier (personnummer or LADOK ID)"
|
|
176
|
+
"student_id", nargs="?", help="Student identifier (personnummer or LADOK ID)."
|
|
111
177
|
)
|
|
112
178
|
|
|
113
|
-
one_parser.add_argument("grade", nargs="?", help="The grade (e.g. A or P)")
|
|
179
|
+
one_parser.add_argument("grade", nargs="?", help="The grade (e.g. A or P).")
|
|
114
180
|
one_parser.add_argument(
|
|
115
181
|
"date",
|
|
116
182
|
nargs="?",
|
|
117
183
|
help="Date on ISO format (e.g. 2021-03-18), "
|
|
118
|
-
f"defaults to today's date ({datetime.date.today()})",
|
|
184
|
+
f"defaults to today's date ({datetime.date.today()}).",
|
|
119
185
|
type=datetime.date.fromisoformat,
|
|
120
186
|
default=datetime.date.today(),
|
|
121
187
|
)
|
|
@@ -156,6 +222,12 @@ def add_command_options(parser):
|
|
|
156
222
|
|
|
157
223
|
|
|
158
224
|
def command(ladok, args):
|
|
225
|
+
"""Execute the result reporting command.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
ladok (LadokSession): The LADOK session for data access.
|
|
229
|
+
args: Parsed command line arguments containing result data or CSV options.
|
|
230
|
+
"""
|
|
159
231
|
if args.course_code:
|
|
160
232
|
report_one_result(ladok, args)
|
|
161
233
|
else:
|