ladok3 4.17__py3-none-any.whl → 4.19__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.
Potentially problematic release.
This version of ladok3 might be problematic. Click here for more details.
- doc/ltxobj/ladok3.pdf +0 -0
- ladok3/__init__.py +619 -3130
- ladok3/api.nw +1001 -10
- ladok3/ladok3.nw +70 -4
- ladok3/report.nw +124 -117
- ladok3/report.py +81 -63
- ladok3/student.nw +20 -1
- ladok3/student.py +62 -41
- ladok3/undoc.nw +54 -3111
- {ladok3-4.17.dist-info → ladok3-4.19.dist-info}/LICENSE +1 -1
- {ladok3-4.17.dist-info → ladok3-4.19.dist-info}/METADATA +28 -1
- ladok3-4.19.dist-info/RECORD +21 -0
- ladok3-4.17.dist-info/RECORD +0 -21
- {ladok3-4.17.dist-info → ladok3-4.19.dist-info}/WHEEL +0 -0
- {ladok3-4.17.dist-info → ladok3-4.19.dist-info}/entry_points.txt +0 -0
ladok3/ladok3.nw
CHANGED
|
@@ -57,6 +57,17 @@ ladok = ladok3.LadokSession("KTH Royal Institute of Technology",
|
|
|
57
57
|
test_environment=True) # for experiments
|
|
58
58
|
\end{minted}
|
|
59
59
|
|
|
60
|
+
Yet another way is to use the [[ladok]] command's own credential store.
|
|
61
|
+
\begin{minted}[linenos]{python}
|
|
62
|
+
import ladok3
|
|
63
|
+
import ladok3.cli
|
|
64
|
+
|
|
65
|
+
ladok = ladok3.LadokSession(*ladok3.cli.load_credentials(),
|
|
66
|
+
test_environment=True) # for experiments
|
|
67
|
+
\end{minted}
|
|
68
|
+
Just note that unpacking [[*]] to unpack the tuple that [[load_credentials]]
|
|
69
|
+
returns.
|
|
70
|
+
|
|
60
71
|
This chapter covers how the [[LadokSession]] class work.
|
|
61
72
|
The remaining chapters cover what the [[ladok]] object can be used for.
|
|
62
73
|
\Cref{StudentClasses} covers how we can work with student data.
|
|
@@ -923,9 +934,62 @@ for course in self.ladok.registrations_JSON(self.ladok_id):
|
|
|
923
934
|
|
|
924
935
|
We also want to know if a student is suspended or not.
|
|
925
936
|
We can get a list of suspensions for a student.
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
937
|
+
This list looks like this:
|
|
938
|
+
\begin{minted}{json}
|
|
939
|
+
[
|
|
940
|
+
{"Anteckning": "VJ-2024-...",
|
|
941
|
+
"Giltighetsperiod": {"Slutdatum": "2024-12-03",
|
|
942
|
+
"Startdatum": "2024-09-25", "link": []},
|
|
943
|
+
"LarosateID": 29,
|
|
944
|
+
"OrsakID": 101450,
|
|
945
|
+
"SenastAndradAv": "someone@kth.se",
|
|
946
|
+
"SenastSparad": "2024-09-24T16:45:56.941",
|
|
947
|
+
"StudentUID": "...",
|
|
948
|
+
"Uid": "...",
|
|
949
|
+
"link": [{"method": "GET",
|
|
950
|
+
"uri": "https://api.ladok.se:443/studentinformation/internal/avstangning/...",
|
|
951
|
+
"mediaType": "application/vnd.ladok+xml",
|
|
952
|
+
"rel": "self"}]
|
|
953
|
+
}
|
|
954
|
+
]
|
|
955
|
+
\end{minted}
|
|
956
|
+
Let's make a class for this.
|
|
957
|
+
<<classes>>=
|
|
958
|
+
class Suspension(LadokData):
|
|
959
|
+
"""A suspension"""
|
|
960
|
+
def __init__(self, /, **kwargs):
|
|
961
|
+
super().__init__(**kwargs)
|
|
962
|
+
self.__note = kwargs.pop("Anteckning")
|
|
963
|
+
self.__validity = (
|
|
964
|
+
datetime.date.fromisoformat(kwargs["Giltighetsperiod"]["Startdatum"]),
|
|
965
|
+
datetime.date.fromisoformat(kwargs["Giltighetsperiod"]["Slutdatum"])
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
@property
|
|
969
|
+
def note(self):
|
|
970
|
+
"""
|
|
971
|
+
The note of the suspension. This is usually a case number.
|
|
972
|
+
"""
|
|
973
|
+
return self.__note
|
|
974
|
+
|
|
975
|
+
@property
|
|
976
|
+
def validity(self):
|
|
977
|
+
"""
|
|
978
|
+
A tuple (start, end) of the validity period of the suspension.
|
|
979
|
+
"""
|
|
980
|
+
return self.__validity
|
|
981
|
+
|
|
982
|
+
@property
|
|
983
|
+
def is_current(self):
|
|
984
|
+
"""
|
|
985
|
+
Is True if the suspension is currently valid.
|
|
986
|
+
"""
|
|
987
|
+
return self.validity[0] <= datetime.date.today() <= self.validity[1]
|
|
988
|
+
|
|
989
|
+
def __str__(self):
|
|
990
|
+
return f"{self.validity[0]}--{self.validity[1]}"
|
|
991
|
+
@
|
|
992
|
+
|
|
929
993
|
We never cache this result since we always want the most up-to-date version.
|
|
930
994
|
<<student attribute methods>>=
|
|
931
995
|
@property
|
|
@@ -933,7 +997,9 @@ def suspensions(self):
|
|
|
933
997
|
"""
|
|
934
998
|
The list of the students' suspensions.
|
|
935
999
|
"""
|
|
936
|
-
|
|
1000
|
+
suspensions = self.ladok.get_student_suspensions_JSON(
|
|
1001
|
+
self.ladok_id)["Avstangning"]
|
|
1002
|
+
return [Suspension(**suspension) for suspension in suspensions]
|
|
937
1003
|
@
|
|
938
1004
|
|
|
939
1005
|
|
ladok3/report.nw
CHANGED
|
@@ -83,6 +83,110 @@ report_parser.add_argument("-f", "--finalize",
|
|
|
83
83
|
@
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
\section{Report many results given in standard input}
|
|
87
|
+
|
|
88
|
+
We want to read CSV data from standard input.
|
|
89
|
+
<<report results given in stdin>>=
|
|
90
|
+
data_reader = csv.reader(sys.stdin, delimiter=args.delimiter)
|
|
91
|
+
for course_code, component_code, student_id, grade, date, *graders in data_reader:
|
|
92
|
+
<<report a result read from stdin>>
|
|
93
|
+
@ We need to add an argument for the delimiter.
|
|
94
|
+
<<add many results group arguments>>=
|
|
95
|
+
many_parser.add_argument("-d", "--delimiter",
|
|
96
|
+
default="\t",
|
|
97
|
+
help="The delimiter for the CSV input; "
|
|
98
|
+
"default is a tab character to be compatible with POSIX commands, "
|
|
99
|
+
"use `-d,` or `-d ,` to get comma-separated values.")
|
|
100
|
+
@
|
|
101
|
+
|
|
102
|
+
We also want to handle errors and confirmations.
|
|
103
|
+
When reporting in bulk, we don't want unnecessary errors.
|
|
104
|
+
We also want to have a summary of the changes.
|
|
105
|
+
<<add many results group arguments>>=
|
|
106
|
+
many_parser.add_argument("-v", "--verbose",
|
|
107
|
+
action="count", default=0,
|
|
108
|
+
help="Increases the verbosity of the output: -v will print results that "
|
|
109
|
+
"were reported to standard out. Otherwise only errors are printed.")
|
|
110
|
+
@
|
|
111
|
+
|
|
112
|
+
Then we can actually report the result using the values read from [[stdin]].
|
|
113
|
+
<<report a result read from stdin>>=
|
|
114
|
+
try:
|
|
115
|
+
set_grade(ladok, args,
|
|
116
|
+
student_id, course_code, component_code, grade, date, graders)
|
|
117
|
+
except Exception as err:
|
|
118
|
+
print(f"{course_code} {component_code}={grade} ({date}) {student_id}: "
|
|
119
|
+
f"{err}",
|
|
120
|
+
file=sys.stderr)
|
|
121
|
+
@
|
|
122
|
+
|
|
123
|
+
When we set the grade, there are a few cases that should be handled.
|
|
124
|
+
If the grade isn't attested, we try to change it.
|
|
125
|
+
(This might still fail if the grade is finalized but not attested.)
|
|
126
|
+
If we've selected the verbose option, then we print what we have reported.
|
|
127
|
+
|
|
128
|
+
If the grade was attested, then we check if it's different.
|
|
129
|
+
If it's different, we output this.
|
|
130
|
+
If it's the same, we silently ignore it.
|
|
131
|
+
This is best for bulk reporting, because then we can always try to report for
|
|
132
|
+
all students.
|
|
133
|
+
|
|
134
|
+
We want to report errors as exceptions.
|
|
135
|
+
<<functions>>=
|
|
136
|
+
def set_grade(ladok, args,
|
|
137
|
+
student_id, course_code, component_code, grade, date, graders):
|
|
138
|
+
student = ladok.get_student(student_id)
|
|
139
|
+
<<get [[course]] from [[student]] and [[course_code]]>>
|
|
140
|
+
<<get [[component]] from [[course]] and [[component_code]]>>
|
|
141
|
+
|
|
142
|
+
if not component.attested and component.grade != grade:
|
|
143
|
+
<<ensure [[date]] is a valid date for [[course]]>>
|
|
144
|
+
component.set_grade(grade, date)
|
|
145
|
+
if args.finalize:
|
|
146
|
+
component.finalize(graders)
|
|
147
|
+
if args.verbose:
|
|
148
|
+
print(f"{course_code} {student}: reported "
|
|
149
|
+
f"{component.component} = {component.grade} ({date}) "
|
|
150
|
+
f"by {', '.join(graders)}.")
|
|
151
|
+
elif component.grade != grade:
|
|
152
|
+
raise Exception(f"attested {component.component} "
|
|
153
|
+
f"result {component.grade} ({component.date}) "
|
|
154
|
+
f"is different from {grade} ({date}).")
|
|
155
|
+
@
|
|
156
|
+
|
|
157
|
+
Now we simply want to set those objects up.
|
|
158
|
+
We want to throw exceptions that explain what the problem is if these don't
|
|
159
|
+
exist.
|
|
160
|
+
<<get [[course]] from [[student]] and [[course_code]]>>=
|
|
161
|
+
try:
|
|
162
|
+
course = student.courses(code=course_code)[0]
|
|
163
|
+
except IndexError:
|
|
164
|
+
raise Exception(f"{course_code}: No such course for {student}")
|
|
165
|
+
<<get [[component]] from [[course]] and [[component_code]]>>=
|
|
166
|
+
try:
|
|
167
|
+
component = course.results(component=component_code)[0]
|
|
168
|
+
except IndexError:
|
|
169
|
+
raise Exception(f"{component_code}: no such component for {course_code}")
|
|
170
|
+
@
|
|
171
|
+
|
|
172
|
+
Finally, we want to ensure the date is correct.
|
|
173
|
+
The date must be at the earliest the start of the course.
|
|
174
|
+
The student can't finish any results before the course has started.
|
|
175
|
+
LADOK will not accept that.
|
|
176
|
+
<<ensure [[date]] is a valid date for [[course]]>>=
|
|
177
|
+
if not isinstance(date, datetime.date):
|
|
178
|
+
date = datetime.date.fromisoformat(date)
|
|
179
|
+
|
|
180
|
+
if date < course.start:
|
|
181
|
+
print(f"{course_code} {component_code}={grade} "
|
|
182
|
+
f"({date}) {student}: "
|
|
183
|
+
f"Grade date ({date}) is before "
|
|
184
|
+
f"course start date ({course.start}), "
|
|
185
|
+
f"using course start date instead.")
|
|
186
|
+
date = course.start
|
|
187
|
+
@
|
|
188
|
+
|
|
189
|
+
|
|
86
190
|
\section{Report a result given on command line}
|
|
87
191
|
|
|
88
192
|
If we've chosen to give one result on the command line, then we'll need the
|
|
@@ -91,28 +195,33 @@ following arguments.
|
|
|
91
195
|
We start with the course, component code, the student's ID and grade.
|
|
92
196
|
<<add one result group arguments>>=
|
|
93
197
|
one_parser.add_argument("course_code", nargs="?",
|
|
94
|
-
help="The course code (e.g. DD1315) for which the grade is for"
|
|
198
|
+
help="The course code (e.g. DD1315) for which the grade is for."
|
|
95
199
|
)
|
|
96
200
|
|
|
97
201
|
one_parser.add_argument("component_code", nargs="?",
|
|
98
|
-
help="The component code (e.g. LAB1) for which the grade is for"
|
|
202
|
+
help="The component code (e.g. LAB1) for which the grade is for. "
|
|
203
|
+
"This can be set to the course code (e.g. DD1315) to set the "
|
|
204
|
+
"final grade for the course. But all components must be "
|
|
205
|
+
"certified (attested) before the course grade can be set."
|
|
99
206
|
)
|
|
100
207
|
|
|
101
208
|
one_parser.add_argument("student_id", nargs="?",
|
|
102
|
-
help="Student identifier (personnummer or LADOK ID)"
|
|
209
|
+
help="Student identifier (personnummer or LADOK ID)."
|
|
103
210
|
)
|
|
104
211
|
|
|
105
212
|
one_parser.add_argument("grade", nargs="?",
|
|
106
|
-
help="The grade (e.g. A or P)"
|
|
213
|
+
help="The grade (e.g. A or P)."
|
|
107
214
|
)
|
|
108
|
-
@ We must make them optional like this to make it work with
|
|
109
|
-
alternative, so we must check ourselves that
|
|
215
|
+
@ We must make them optional like this to make it work with our second
|
|
216
|
+
alternative (bulk reporting through [[stdin]]), so we must check ourselves that
|
|
217
|
+
we got the arguments.
|
|
110
218
|
<<check that we got all positional arguments>>=
|
|
111
219
|
if not (args.course_code and args.component_code and
|
|
112
220
|
args.student_id and args.grade):
|
|
113
221
|
print(f"{sys.argv[0]} report: "
|
|
114
|
-
|
|
115
|
-
|
|
222
|
+
"not all positional args given: "
|
|
223
|
+
"course_code, component, student, grade",
|
|
224
|
+
file=sys.stderr)
|
|
116
225
|
sys.exit(1)
|
|
117
226
|
@
|
|
118
227
|
|
|
@@ -123,7 +232,7 @@ If it's not provided, we let [[argparse]] set it to today's date.
|
|
|
123
232
|
<<add one result group arguments>>=
|
|
124
233
|
one_parser.add_argument("date", nargs="?",
|
|
125
234
|
help="Date on ISO format (e.g. 2021-03-18), "
|
|
126
|
-
|
|
235
|
+
f"defaults to today's date ({datetime.date.today()}).",
|
|
127
236
|
type=datetime.date.fromisoformat,
|
|
128
237
|
default=datetime.date.today()
|
|
129
238
|
)
|
|
@@ -141,115 +250,13 @@ them.
|
|
|
141
250
|
<<report results given in args>>=
|
|
142
251
|
<<check that we got all positional arguments>>
|
|
143
252
|
try:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
result.set_grade(args.grade, args.date)
|
|
148
|
-
if args.finalize:
|
|
149
|
-
result.finalize(args.graders)
|
|
253
|
+
set_grade(ladok, args,
|
|
254
|
+
args.student_id, args.course_code, args.component_code,
|
|
255
|
+
args.grade, args.date, args.graders)
|
|
150
256
|
except Exception as err:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
print(f"{verr}: {args.student_id}: {err}")
|
|
155
|
-
@ The option [[args.finalize]] is already set above, so we don't need to add
|
|
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}).")
|
|
257
|
+
print(f"{args.course_code} {args.component_code}={args.grade} ({args.date}) "
|
|
258
|
+
f"{args.student_id}: {err}",
|
|
259
|
+
file=sys.stderr)
|
|
253
260
|
@
|
|
254
261
|
|
|
255
262
|
|
ladok3/report.py
CHANGED
|
@@ -10,78 +10,93 @@ def report_one_result(ladok, args):
|
|
|
10
10
|
):
|
|
11
11
|
print(
|
|
12
12
|
f"{sys.argv[0]} report: "
|
|
13
|
-
"not all positional args given:
|
|
13
|
+
"not all positional args given: "
|
|
14
|
+
"course_code, component, student, grade",
|
|
14
15
|
file=sys.stderr,
|
|
15
16
|
)
|
|
16
17
|
sys.exit(1)
|
|
17
18
|
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)
|
|
19
|
+
set_grade(
|
|
20
|
+
ladok,
|
|
21
|
+
args,
|
|
22
|
+
args.student_id,
|
|
23
|
+
args.course_code,
|
|
24
|
+
args.component_code,
|
|
25
|
+
args.grade,
|
|
26
|
+
args.date,
|
|
27
|
+
args.graders,
|
|
28
|
+
)
|
|
32
29
|
except Exception as err:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
print(
|
|
31
|
+
f"{args.course_code} {args.component_code}={args.grade} ({args.date}) "
|
|
32
|
+
f"{args.student_id}: {err}",
|
|
33
|
+
file=sys.stderr,
|
|
34
|
+
)
|
|
37
35
|
|
|
38
36
|
|
|
39
37
|
def report_many_results(ladok, args):
|
|
40
38
|
data_reader = csv.reader(sys.stdin, delimiter=args.delimiter)
|
|
41
39
|
for course_code, component_code, student_id, grade, date, *graders in data_reader:
|
|
42
40
|
try:
|
|
43
|
-
|
|
41
|
+
set_grade(
|
|
42
|
+
ladok,
|
|
43
|
+
args,
|
|
44
|
+
student_id,
|
|
45
|
+
course_code,
|
|
46
|
+
component_code,
|
|
47
|
+
grade,
|
|
48
|
+
date,
|
|
49
|
+
graders,
|
|
50
|
+
)
|
|
51
|
+
except Exception as err:
|
|
52
|
+
print(
|
|
53
|
+
f"{course_code} {component_code}={grade} ({date}) {student_id}: "
|
|
54
|
+
f"{err}",
|
|
55
|
+
file=sys.stderr,
|
|
56
|
+
)
|
|
44
57
|
|
|
45
|
-
try:
|
|
46
|
-
course = student.courses(code=course_code)[0]
|
|
47
|
-
except IndexError:
|
|
48
|
-
raise Exception(f"{course_code}: No such course for {student}")
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
def set_grade(
|
|
60
|
+
ladok, args, student_id, course_code, component_code, grade, date, graders
|
|
61
|
+
):
|
|
62
|
+
student = ladok.get_student(student_id)
|
|
63
|
+
try:
|
|
64
|
+
course = student.courses(code=course_code)[0]
|
|
65
|
+
except IndexError:
|
|
66
|
+
raise Exception(f"{course_code}: No such course for {student}")
|
|
67
|
+
try:
|
|
68
|
+
component = course.results(component=component_code)[0]
|
|
69
|
+
except IndexError:
|
|
70
|
+
raise Exception(f"{component_code}: no such component for {course_code}")
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
if not component.attested and component.grade != grade:
|
|
73
|
+
if not isinstance(date, datetime.date):
|
|
74
|
+
date = datetime.date.fromisoformat(date)
|
|
75
|
+
|
|
76
|
+
if date < course.start:
|
|
77
|
+
print(
|
|
78
|
+
f"{course_code} {component_code}={grade} "
|
|
79
|
+
f"({date}) {student}: "
|
|
80
|
+
f"Grade date ({date}) is before "
|
|
81
|
+
f"course start date ({course.start}), "
|
|
82
|
+
f"using course start date instead."
|
|
83
|
+
)
|
|
84
|
+
date = course.start
|
|
85
|
+
component.set_grade(grade, date)
|
|
86
|
+
if args.finalize:
|
|
87
|
+
component.finalize(graders)
|
|
88
|
+
if args.verbose:
|
|
89
|
+
print(
|
|
90
|
+
f"{course_code} {student}: reported "
|
|
91
|
+
f"{component.component} = {component.grade} ({date}) "
|
|
92
|
+
f"by {', '.join(graders)}."
|
|
93
|
+
)
|
|
94
|
+
elif component.grade != grade:
|
|
95
|
+
raise Exception(
|
|
96
|
+
f"attested {component.component} "
|
|
97
|
+
f"result {component.grade} ({component.date}) "
|
|
98
|
+
f"is different from {grade} ({date})."
|
|
99
|
+
)
|
|
85
100
|
|
|
86
101
|
|
|
87
102
|
def add_command_options(parser):
|
|
@@ -97,25 +112,28 @@ def add_command_options(parser):
|
|
|
97
112
|
one_parser.add_argument(
|
|
98
113
|
"course_code",
|
|
99
114
|
nargs="?",
|
|
100
|
-
help="The course code (e.g. DD1315) for which the grade is for",
|
|
115
|
+
help="The course code (e.g. DD1315) for which the grade is for.",
|
|
101
116
|
)
|
|
102
117
|
|
|
103
118
|
one_parser.add_argument(
|
|
104
119
|
"component_code",
|
|
105
120
|
nargs="?",
|
|
106
|
-
help="The component code (e.g. LAB1) for which the grade is for"
|
|
121
|
+
help="The component code (e.g. LAB1) for which the grade is for. "
|
|
122
|
+
"This can be set to the course code (e.g. DD1315) to set the "
|
|
123
|
+
"final grade for the course. But all components must be "
|
|
124
|
+
"certified (attested) before the course grade can be set.",
|
|
107
125
|
)
|
|
108
126
|
|
|
109
127
|
one_parser.add_argument(
|
|
110
|
-
"student_id", nargs="?", help="Student identifier (personnummer or LADOK ID)"
|
|
128
|
+
"student_id", nargs="?", help="Student identifier (personnummer or LADOK ID)."
|
|
111
129
|
)
|
|
112
130
|
|
|
113
|
-
one_parser.add_argument("grade", nargs="?", help="The grade (e.g. A or P)")
|
|
131
|
+
one_parser.add_argument("grade", nargs="?", help="The grade (e.g. A or P).")
|
|
114
132
|
one_parser.add_argument(
|
|
115
133
|
"date",
|
|
116
134
|
nargs="?",
|
|
117
135
|
help="Date on ISO format (e.g. 2021-03-18), "
|
|
118
|
-
f"defaults to today's date ({datetime.date.today()})",
|
|
136
|
+
f"defaults to today's date ({datetime.date.today()}).",
|
|
119
137
|
type=datetime.date.fromisoformat,
|
|
120
138
|
default=datetime.date.today(),
|
|
121
139
|
)
|
ladok3/student.nw
CHANGED
|
@@ -106,9 +106,28 @@ def print_student_data(student):
|
|
|
106
106
|
print(f"Personnummer: {student.personnummer}")
|
|
107
107
|
print(f"LADOK ID: {student.ladok_id}")
|
|
108
108
|
print(f"Alive: {student.alive}")
|
|
109
|
-
print
|
|
109
|
+
<<print info about suspensions for [[student]]>>
|
|
110
110
|
@
|
|
111
111
|
|
|
112
|
+
We want to print whether the student is currently suspended or not.
|
|
113
|
+
We want to make this really clear.
|
|
114
|
+
<<print info about suspensions for [[student]]>>=
|
|
115
|
+
print(f"Suspended: ", end="")
|
|
116
|
+
if any(map(lambda x: x.is_current, student.suspensions)):
|
|
117
|
+
print("YES")
|
|
118
|
+
else:
|
|
119
|
+
print("no")
|
|
120
|
+
@ Then we also want to print all the times the student has been suspended.
|
|
121
|
+
We only want to print this if the student has been suspended at least once.
|
|
122
|
+
<<print info about suspensions for [[student]]>>=
|
|
123
|
+
if student.suspensions:
|
|
124
|
+
print(f"Suspenions: ", end="")
|
|
125
|
+
for suspension in student.suspensions:
|
|
126
|
+
print(f"{suspension}", end="\n ")
|
|
127
|
+
print()
|
|
128
|
+
@
|
|
129
|
+
|
|
130
|
+
|
|
112
131
|
\subsection{Printing student's course data}
|
|
113
132
|
|
|
114
133
|
To print the student's course data, we simply filter the courses on the option
|