ladok3 4.18__py3-none-any.whl → 5.0__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/data.nw +26 -8
- ladok3/data.py +23 -3
- ladok3/report.nw +139 -117
- ladok3/report.py +88 -62
- {ladok3-4.18.dist-info → ladok3-5.0.dist-info}/METADATA +1 -1
- {ladok3-4.18.dist-info → ladok3-5.0.dist-info}/RECORD +10 -10
- {ladok3-4.18.dist-info → ladok3-5.0.dist-info}/LICENSE +0 -0
- {ladok3-4.18.dist-info → ladok3-5.0.dist-info}/WHEEL +0 -0
- {ladok3-4.18.dist-info → ladok3-5.0.dist-info}/entry_points.txt +0 -0
doc/ltxobj/ladok3.pdf
CHANGED
|
Binary file
|
ladok3/data.nw
CHANGED
|
@@ -16,7 +16,7 @@ from LADOK (\cref{DataCommand}).
|
|
|
16
16
|
This is a subcommand for the [[ladok]] command-line interface.
|
|
17
17
|
It can be used like this:
|
|
18
18
|
\begin{minted}{bash}
|
|
19
|
-
ladok
|
|
19
|
+
ladok course DD1315 > DD1315.csv
|
|
20
20
|
\end{minted}
|
|
21
21
|
This program will produce CSV-formated data to answer the questions above.
|
|
22
22
|
The data is formated like this:
|
|
@@ -26,11 +26,11 @@ The data is formated like this:
|
|
|
26
26
|
\item component,
|
|
27
27
|
\item student (pseudonym),
|
|
28
28
|
\item grade,
|
|
29
|
-
\item normalized
|
|
29
|
+
\item either absolute date or normalized to the course start and finish.
|
|
30
30
|
\end{itemize}
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
\section{The [[
|
|
33
|
+
\section{The [[course]] subcommand}\label{DataCommand}
|
|
34
34
|
|
|
35
35
|
This is a subcommand run as part of the [[ladok3.cli]] module.
|
|
36
36
|
We provide a function [[add_command_options]] that adds the subcommand options
|
|
@@ -59,7 +59,7 @@ def command(ladok, args):
|
|
|
59
59
|
We add a subparser.
|
|
60
60
|
We set it up to use the function [[command]].
|
|
61
61
|
<<add data parser to parser>>=
|
|
62
|
-
data_parser = parser.add_parser("
|
|
62
|
+
data_parser = parser.add_parser("course",
|
|
63
63
|
help="Returns course results data in CSV form",
|
|
64
64
|
description="""
|
|
65
65
|
Returns the results in CSV form for all first-time registered students.
|
|
@@ -79,9 +79,10 @@ course_rounds = filter_rounds(
|
|
|
79
79
|
ladok.search_course_rounds(code=args.course_code),
|
|
80
80
|
args.rounds)
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
if args.header:
|
|
83
|
+
data_writer.writerow([
|
|
84
|
+
"Course", "Round", "Component", "Student", "Grade", "Time"
|
|
85
|
+
])
|
|
85
86
|
for course_round in course_rounds:
|
|
86
87
|
data = extract_data_for_round(ladok, course_round, args)
|
|
87
88
|
|
|
@@ -91,6 +92,7 @@ for course_round in course_rounds:
|
|
|
91
92
|
student, grade, time]
|
|
92
93
|
)
|
|
93
94
|
@ We must take a course code and a delimiter as arguments.
|
|
95
|
+
We also want to know if we want a header or not.
|
|
94
96
|
<<add data command arguments to data parser>>=
|
|
95
97
|
data_parser.add_argument("course_code",
|
|
96
98
|
help="The course code of the course for which to export data")
|
|
@@ -100,6 +102,9 @@ data_parser.add_argument("-d", "--delimiter",
|
|
|
100
102
|
help="The delimiter for the CSV output; "
|
|
101
103
|
"default is a tab character to be compatible with POSIX commands, "
|
|
102
104
|
"use `-d,` or `-d ,` to get comma-separated values.")
|
|
105
|
+
|
|
106
|
+
data_parser.add_argument("-H", "--header", action="store_true",
|
|
107
|
+
help="Print a header line with the column names.")
|
|
103
108
|
@
|
|
104
109
|
|
|
105
110
|
We filter the rounds.
|
|
@@ -154,7 +159,20 @@ def extract_data_for_round(ladok, course_round, args):
|
|
|
154
159
|
else:
|
|
155
160
|
<<extract grade and normalized date from result data>>
|
|
156
161
|
|
|
157
|
-
yield student, component, grade
|
|
162
|
+
<<yield [[student, component, grade]] and date>>
|
|
163
|
+
@
|
|
164
|
+
|
|
165
|
+
We want to yield the data in CSV form, so we simply yield a tuple.
|
|
166
|
+
The date is either the normalized date or the date from the result data.
|
|
167
|
+
<<yield [[student, component, grade]] and date>>=
|
|
168
|
+
if args.normalize_date:
|
|
169
|
+
yield student, component, grade, normalized_date
|
|
170
|
+
elif result_data:
|
|
171
|
+
yield student, component, grade, result_data["Examinationsdatum"]
|
|
172
|
+
<<add data command arguments to data parser>>=
|
|
173
|
+
data_parser.add_argument("-n", "--normalize-date", action="store_true",
|
|
174
|
+
help="Normalize the date to the start of the course, "
|
|
175
|
+
"otherwise the date is printed as is.")
|
|
158
176
|
@
|
|
159
177
|
|
|
160
178
|
\subsection{Get round data}
|
ladok3/data.py
CHANGED
|
@@ -60,7 +60,10 @@ def extract_data_for_round(ladok, course_round, args):
|
|
|
60
60
|
grade = "-"
|
|
61
61
|
normalized_date = None
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
if args.normalize_date:
|
|
64
|
+
yield student, component, grade, normalized_date
|
|
65
|
+
elif result_data:
|
|
66
|
+
yield student, component, grade, result_data["Examinationsdatum"]
|
|
64
67
|
|
|
65
68
|
|
|
66
69
|
def filter_student_results(student, results):
|
|
@@ -132,7 +135,7 @@ def has_credit_transfer(results):
|
|
|
132
135
|
|
|
133
136
|
def add_command_options(parser):
|
|
134
137
|
data_parser = parser.add_parser(
|
|
135
|
-
"
|
|
138
|
+
"course",
|
|
136
139
|
help="Returns course results data in CSV form",
|
|
137
140
|
description="""
|
|
138
141
|
Returns the results in CSV form for all first-time registered students.
|
|
@@ -151,6 +154,13 @@ def add_command_options(parser):
|
|
|
151
154
|
"default is a tab character to be compatible with POSIX commands, "
|
|
152
155
|
"use `-d,` or `-d ,` to get comma-separated values.",
|
|
153
156
|
)
|
|
157
|
+
|
|
158
|
+
data_parser.add_argument(
|
|
159
|
+
"-H",
|
|
160
|
+
"--header",
|
|
161
|
+
action="store_true",
|
|
162
|
+
help="Print a header line with the column names.",
|
|
163
|
+
)
|
|
154
164
|
data_parser.add_argument(
|
|
155
165
|
"-r",
|
|
156
166
|
"--rounds",
|
|
@@ -158,6 +168,13 @@ def add_command_options(parser):
|
|
|
158
168
|
help="The round codes for the rounds to include, "
|
|
159
169
|
"otherwise all rounds will be included.",
|
|
160
170
|
)
|
|
171
|
+
data_parser.add_argument(
|
|
172
|
+
"-n",
|
|
173
|
+
"--normalize-date",
|
|
174
|
+
action="store_true",
|
|
175
|
+
help="Normalize the date to the start of the course, "
|
|
176
|
+
"otherwise the date is printed as is.",
|
|
177
|
+
)
|
|
161
178
|
data_parser.add_argument(
|
|
162
179
|
"-t",
|
|
163
180
|
"--time-limit",
|
|
@@ -188,7 +205,10 @@ def command(ladok, args):
|
|
|
188
205
|
ladok.search_course_rounds(code=args.course_code), args.rounds
|
|
189
206
|
)
|
|
190
207
|
|
|
191
|
-
|
|
208
|
+
if args.header:
|
|
209
|
+
data_writer.writerow(
|
|
210
|
+
["Course", "Round", "Component", "Student", "Grade", "Time"]
|
|
211
|
+
)
|
|
192
212
|
for course_round in course_rounds:
|
|
193
213
|
data = extract_data_for_round(ladok, course_round, args)
|
|
194
214
|
|
ladok3/report.nw
CHANGED
|
@@ -83,6 +83,123 @@ 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
|
+
<<try to resolve [[student]] from [[ladok]] using [[student_id]]>>
|
|
119
|
+
print(f"{course_code} {component_code}={grade} ({date}) {student}: "
|
|
120
|
+
f"{err}",
|
|
121
|
+
file=sys.stderr)
|
|
122
|
+
@
|
|
123
|
+
|
|
124
|
+
The reason we want to resolve the student from LADOK is that the [[student_id]]
|
|
125
|
+
might be a personnummer or a LADOK ID---if the latter, it's not particularly
|
|
126
|
+
readable for a human and we can't use the LADOK ID in the LADOK web interface
|
|
127
|
+
when we want to deal with the errors manually.
|
|
128
|
+
But if we resolve the student, then we get a readable name.
|
|
129
|
+
<<try to resolve [[student]] from [[ladok]] using [[student_id]]>>=
|
|
130
|
+
try:
|
|
131
|
+
student = ladok.get_student(student_id)
|
|
132
|
+
except Exception:
|
|
133
|
+
student = student_id
|
|
134
|
+
@
|
|
135
|
+
|
|
136
|
+
When we set the grade, there are a few cases that should be handled.
|
|
137
|
+
If the grade isn't attested, we try to change it.
|
|
138
|
+
(This might still fail if the grade is finalized but not attested.)
|
|
139
|
+
If we've selected the verbose option, then we print what we have reported.
|
|
140
|
+
|
|
141
|
+
If the grade was attested, then we check if it's different.
|
|
142
|
+
If it's different, we output this.
|
|
143
|
+
If it's the same, we silently ignore it.
|
|
144
|
+
This is best for bulk reporting, because then we can always try to report for
|
|
145
|
+
all students.
|
|
146
|
+
|
|
147
|
+
We want to report errors as exceptions.
|
|
148
|
+
<<functions>>=
|
|
149
|
+
def set_grade(ladok, args,
|
|
150
|
+
student_id, course_code, component_code, grade, date, graders):
|
|
151
|
+
student = ladok.get_student(student_id)
|
|
152
|
+
<<get [[course]] from [[student]] and [[course_code]]>>
|
|
153
|
+
<<get [[component]] from [[course]] and [[component_code]]>>
|
|
154
|
+
|
|
155
|
+
if not component.attested and component.grade != grade:
|
|
156
|
+
<<ensure [[date]] is a valid date for [[course]]>>
|
|
157
|
+
component.set_grade(grade, date)
|
|
158
|
+
if args.finalize:
|
|
159
|
+
component.finalize(graders)
|
|
160
|
+
if args.verbose:
|
|
161
|
+
print(f"{course_code} {student}: reported "
|
|
162
|
+
f"{component.component} = {component.grade} ({date}) "
|
|
163
|
+
f"by {', '.join(graders)}.")
|
|
164
|
+
elif component.grade != grade:
|
|
165
|
+
raise Exception(f"attested {component.component} "
|
|
166
|
+
f"result {component.grade} ({component.date}) "
|
|
167
|
+
f"is different from {grade} ({date}).")
|
|
168
|
+
@
|
|
169
|
+
|
|
170
|
+
Now we simply want to set those objects up.
|
|
171
|
+
We want to throw exceptions that explain what the problem is if these don't
|
|
172
|
+
exist.
|
|
173
|
+
<<get [[course]] from [[student]] and [[course_code]]>>=
|
|
174
|
+
try:
|
|
175
|
+
course = student.courses(code=course_code)[0]
|
|
176
|
+
except IndexError:
|
|
177
|
+
raise Exception(f"{course_code}: No such course for {student}")
|
|
178
|
+
<<get [[component]] from [[course]] and [[component_code]]>>=
|
|
179
|
+
try:
|
|
180
|
+
component = course.results(component=component_code)[0]
|
|
181
|
+
except IndexError:
|
|
182
|
+
raise Exception(f"{component_code}: no such component for {course_code}")
|
|
183
|
+
@
|
|
184
|
+
|
|
185
|
+
Finally, we want to ensure the date is correct.
|
|
186
|
+
The date must be at the earliest the start of the course.
|
|
187
|
+
The student can't finish any results before the course has started.
|
|
188
|
+
LADOK will not accept that.
|
|
189
|
+
<<ensure [[date]] is a valid date for [[course]]>>=
|
|
190
|
+
if not isinstance(date, datetime.date):
|
|
191
|
+
date = datetime.date.fromisoformat(date)
|
|
192
|
+
|
|
193
|
+
if date < course.start:
|
|
194
|
+
print(f"{course_code} {component_code}={grade} "
|
|
195
|
+
f"({date}) {student}: "
|
|
196
|
+
f"Grade date ({date}) is before "
|
|
197
|
+
f"course start date ({course.start}), "
|
|
198
|
+
f"using course start date instead.")
|
|
199
|
+
date = course.start
|
|
200
|
+
@
|
|
201
|
+
|
|
202
|
+
|
|
86
203
|
\section{Report a result given on command line}
|
|
87
204
|
|
|
88
205
|
If we've chosen to give one result on the command line, then we'll need the
|
|
@@ -91,28 +208,33 @@ following arguments.
|
|
|
91
208
|
We start with the course, component code, the student's ID and grade.
|
|
92
209
|
<<add one result group arguments>>=
|
|
93
210
|
one_parser.add_argument("course_code", nargs="?",
|
|
94
|
-
help="The course code (e.g. DD1315) for which the grade is for"
|
|
211
|
+
help="The course code (e.g. DD1315) for which the grade is for."
|
|
95
212
|
)
|
|
96
213
|
|
|
97
214
|
one_parser.add_argument("component_code", nargs="?",
|
|
98
|
-
help="The component code (e.g. LAB1) for which the grade is for"
|
|
215
|
+
help="The component code (e.g. LAB1) for which the grade is for. "
|
|
216
|
+
"This can be set to the course code (e.g. DD1315) to set the "
|
|
217
|
+
"final grade for the course. But all components must be "
|
|
218
|
+
"certified (attested) before the course grade can be set."
|
|
99
219
|
)
|
|
100
220
|
|
|
101
221
|
one_parser.add_argument("student_id", nargs="?",
|
|
102
|
-
help="Student identifier (personnummer or LADOK ID)"
|
|
222
|
+
help="Student identifier (personnummer or LADOK ID)."
|
|
103
223
|
)
|
|
104
224
|
|
|
105
225
|
one_parser.add_argument("grade", nargs="?",
|
|
106
|
-
help="The grade (e.g. A or P)"
|
|
226
|
+
help="The grade (e.g. A or P)."
|
|
107
227
|
)
|
|
108
|
-
@ We must make them optional like this to make it work with
|
|
109
|
-
alternative, so we must check ourselves that
|
|
228
|
+
@ We must make them optional like this to make it work with our second
|
|
229
|
+
alternative (bulk reporting through [[stdin]]), so we must check ourselves that
|
|
230
|
+
we got the arguments.
|
|
110
231
|
<<check that we got all positional arguments>>=
|
|
111
232
|
if not (args.course_code and args.component_code and
|
|
112
233
|
args.student_id and args.grade):
|
|
113
234
|
print(f"{sys.argv[0]} report: "
|
|
114
|
-
|
|
115
|
-
|
|
235
|
+
"not all positional args given: "
|
|
236
|
+
"course_code, component, student, grade",
|
|
237
|
+
file=sys.stderr)
|
|
116
238
|
sys.exit(1)
|
|
117
239
|
@
|
|
118
240
|
|
|
@@ -123,7 +245,7 @@ If it's not provided, we let [[argparse]] set it to today's date.
|
|
|
123
245
|
<<add one result group arguments>>=
|
|
124
246
|
one_parser.add_argument("date", nargs="?",
|
|
125
247
|
help="Date on ISO format (e.g. 2021-03-18), "
|
|
126
|
-
|
|
248
|
+
f"defaults to today's date ({datetime.date.today()}).",
|
|
127
249
|
type=datetime.date.fromisoformat,
|
|
128
250
|
default=datetime.date.today()
|
|
129
251
|
)
|
|
@@ -141,115 +263,15 @@ them.
|
|
|
141
263
|
<<report results given in args>>=
|
|
142
264
|
<<check that we got all positional arguments>>
|
|
143
265
|
try:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
result.set_grade(args.grade, args.date)
|
|
148
|
-
if args.finalize:
|
|
149
|
-
result.finalize(args.graders)
|
|
266
|
+
set_grade(ladok, args,
|
|
267
|
+
args.student_id, args.course_code, args.component_code,
|
|
268
|
+
args.grade, args.date, args.graders)
|
|
150
269
|
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}).")
|
|
270
|
+
student_id = args.student_id
|
|
271
|
+
<<try to resolve [[student]] from [[ladok]] using [[student_id]]>>
|
|
272
|
+
print(f"{args.course_code} {args.component_code}={args.grade} ({args.date}) "
|
|
273
|
+
f"{student}: {err}",
|
|
274
|
+
file=sys.stderr)
|
|
253
275
|
@
|
|
254
276
|
|
|
255
277
|
|
ladok3/report.py
CHANGED
|
@@ -10,78 +10,101 @@ 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:
|
|
30
|
+
student_id = args.student_id
|
|
33
31
|
try:
|
|
34
|
-
|
|
35
|
-
except
|
|
36
|
-
|
|
32
|
+
student = ladok.get_student(student_id)
|
|
33
|
+
except Exception:
|
|
34
|
+
student = student_id
|
|
35
|
+
print(
|
|
36
|
+
f"{args.course_code} {args.component_code}={args.grade} ({args.date}) "
|
|
37
|
+
f"{student}: {err}",
|
|
38
|
+
file=sys.stderr,
|
|
39
|
+
)
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
def report_many_results(ladok, args):
|
|
40
43
|
data_reader = csv.reader(sys.stdin, delimiter=args.delimiter)
|
|
41
44
|
for course_code, component_code, student_id, grade, date, *graders in data_reader:
|
|
42
45
|
try:
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
set_grade(
|
|
47
|
+
ladok,
|
|
48
|
+
args,
|
|
49
|
+
student_id,
|
|
50
|
+
course_code,
|
|
51
|
+
component_code,
|
|
52
|
+
grade,
|
|
53
|
+
date,
|
|
54
|
+
graders,
|
|
55
|
+
)
|
|
56
|
+
except Exception as err:
|
|
45
57
|
try:
|
|
46
|
-
|
|
47
|
-
except
|
|
48
|
-
|
|
58
|
+
student = ladok.get_student(student_id)
|
|
59
|
+
except Exception:
|
|
60
|
+
student = student_id
|
|
61
|
+
print(
|
|
62
|
+
f"{course_code} {component_code}={grade} ({date}) {student}: " f"{err}",
|
|
63
|
+
file=sys.stderr,
|
|
64
|
+
)
|
|
49
65
|
|
|
50
|
-
try:
|
|
51
|
-
component = course.results(component=component_code)[0]
|
|
52
|
-
except IndexError:
|
|
53
|
-
raise Exception(
|
|
54
|
-
f"{component_code}: no such component for {course_code}"
|
|
55
|
-
)
|
|
56
66
|
|
|
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
|
-
|
|
67
|
+
def set_grade(
|
|
68
|
+
ladok, args, student_id, course_code, component_code, grade, date, graders
|
|
69
|
+
):
|
|
70
|
+
student = ladok.get_student(student_id)
|
|
71
|
+
try:
|
|
72
|
+
course = student.courses(code=course_code)[0]
|
|
73
|
+
except IndexError:
|
|
74
|
+
raise Exception(f"{course_code}: No such course for {student}")
|
|
75
|
+
try:
|
|
76
|
+
component = course.results(component=component_code)[0]
|
|
77
|
+
except IndexError:
|
|
78
|
+
raise Exception(f"{component_code}: no such component for {course_code}")
|
|
79
|
+
|
|
80
|
+
if not component.attested and component.grade != grade:
|
|
81
|
+
if not isinstance(date, datetime.date):
|
|
82
|
+
date = datetime.date.fromisoformat(date)
|
|
83
|
+
|
|
84
|
+
if date < course.start:
|
|
85
|
+
print(
|
|
86
|
+
f"{course_code} {component_code}={grade} "
|
|
87
|
+
f"({date}) {student}: "
|
|
88
|
+
f"Grade date ({date}) is before "
|
|
89
|
+
f"course start date ({course.start}), "
|
|
90
|
+
f"using course start date instead."
|
|
91
|
+
)
|
|
92
|
+
date = course.start
|
|
93
|
+
component.set_grade(grade, date)
|
|
94
|
+
if args.finalize:
|
|
95
|
+
component.finalize(graders)
|
|
96
|
+
if args.verbose:
|
|
97
|
+
print(
|
|
98
|
+
f"{course_code} {student}: reported "
|
|
99
|
+
f"{component.component} = {component.grade} ({date}) "
|
|
100
|
+
f"by {', '.join(graders)}."
|
|
101
|
+
)
|
|
102
|
+
elif component.grade != grade:
|
|
103
|
+
raise Exception(
|
|
104
|
+
f"attested {component.component} "
|
|
105
|
+
f"result {component.grade} ({component.date}) "
|
|
106
|
+
f"is different from {grade} ({date})."
|
|
107
|
+
)
|
|
85
108
|
|
|
86
109
|
|
|
87
110
|
def add_command_options(parser):
|
|
@@ -97,25 +120,28 @@ def add_command_options(parser):
|
|
|
97
120
|
one_parser.add_argument(
|
|
98
121
|
"course_code",
|
|
99
122
|
nargs="?",
|
|
100
|
-
help="The course code (e.g. DD1315) for which the grade is for",
|
|
123
|
+
help="The course code (e.g. DD1315) for which the grade is for.",
|
|
101
124
|
)
|
|
102
125
|
|
|
103
126
|
one_parser.add_argument(
|
|
104
127
|
"component_code",
|
|
105
128
|
nargs="?",
|
|
106
|
-
help="The component code (e.g. LAB1) for which the grade is for"
|
|
129
|
+
help="The component code (e.g. LAB1) for which the grade is for. "
|
|
130
|
+
"This can be set to the course code (e.g. DD1315) to set the "
|
|
131
|
+
"final grade for the course. But all components must be "
|
|
132
|
+
"certified (attested) before the course grade can be set.",
|
|
107
133
|
)
|
|
108
134
|
|
|
109
135
|
one_parser.add_argument(
|
|
110
|
-
"student_id", nargs="?", help="Student identifier (personnummer or LADOK ID)"
|
|
136
|
+
"student_id", nargs="?", help="Student identifier (personnummer or LADOK ID)."
|
|
111
137
|
)
|
|
112
138
|
|
|
113
|
-
one_parser.add_argument("grade", nargs="?", help="The grade (e.g. A or P)")
|
|
139
|
+
one_parser.add_argument("grade", nargs="?", help="The grade (e.g. A or P).")
|
|
114
140
|
one_parser.add_argument(
|
|
115
141
|
"date",
|
|
116
142
|
nargs="?",
|
|
117
143
|
help="Date on ISO format (e.g. 2021-03-18), "
|
|
118
|
-
f"defaults to today's date ({datetime.date.today()})",
|
|
144
|
+
f"defaults to today's date ({datetime.date.today()}).",
|
|
119
145
|
type=datetime.date.fromisoformat,
|
|
120
146
|
default=datetime.date.today(),
|
|
121
147
|
)
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
doc/ltxobj/ladok3.pdf,sha256=
|
|
1
|
+
doc/ltxobj/ladok3.pdf,sha256=5veFT8zO_oUWLCBQVqmwLRWsy3_34dZms1RgitsNzrM,2284649
|
|
2
2
|
ladok3/.gitignore,sha256=QOcCshtjIsFasi4vaqFcooBWPJkxVWaoYEWOBBtdY_w,81
|
|
3
3
|
ladok3/Makefile,sha256=Jy6OFjoVLU9YivnVxctxI_zrUOK9ysEOiistJ3ST6Nw,557
|
|
4
4
|
ladok3/__init__.py,sha256=2ySW9vSJ0joOV5TH9dtK_CAOdk1PByZ7jYfAwQxfAhc,87708
|
|
5
5
|
ladok3/api.nw,sha256=o7ZWO6eplSG3ReS0Y-rzcrLP2yiFVwvQUtg6dPDXG5E,66716
|
|
6
6
|
ladok3/cli.nw,sha256=sA5kevvAWBkzZHZ3UBkPEHoWvPw5yYtGYiPEnWMmjus,22864
|
|
7
7
|
ladok3/cli.py,sha256=6R7B0XVvYFlHcMbv6E73KOeI3ZGQFeQN2-ojz12H1vU,12555
|
|
8
|
-
ladok3/data.nw,sha256=
|
|
9
|
-
ladok3/data.py,sha256=
|
|
8
|
+
ladok3/data.nw,sha256=NddmEnzfgaXZkYyaOLle67MNySy4YKWx4OXMWAOAMkA,12676
|
|
9
|
+
ladok3/data.py,sha256=nLta8ZlSmgRqXZoCbJqtw6KlHH-EyZfImbDcPs74G2U,7324
|
|
10
10
|
ladok3/ladok.bash,sha256=zGfTFdtos2zLjV13pzfK-1uCy2b_lF2qUKMoL2ExW7c,1441
|
|
11
11
|
ladok3/ladok3.nw,sha256=mftysbBD9CLaKD8JgRcSO9KOnjV6koOd2dfmbSSJJi0,55594
|
|
12
|
-
ladok3/report.nw,sha256=
|
|
13
|
-
ladok3/report.py,sha256=
|
|
12
|
+
ladok3/report.nw,sha256=nmyP6TnSYBsMIeG2bdiVQ7LqQN_gEqyEfbDrrOR1afc,10068
|
|
13
|
+
ladok3/report.py,sha256=BDFbTsO2mwFQoCysRnak2VYA3p4RYOnxaU6zamhqbAY,6091
|
|
14
14
|
ladok3/student.nw,sha256=qgJN1IA6oIoI4RsfWsoTaO2yxdtB_y3mBNSmlccoE4Y,4395
|
|
15
15
|
ladok3/student.py,sha256=a3z5dY18UXqxZh4WtEhgsUdHePhRNBiU5GaNNP6LKxI,2178
|
|
16
16
|
ladok3/undoc.nw,sha256=rRAZ8fC44PD2hBDF8TeEg1hyge_-vd-ScRcojRXQycI,19841
|
|
17
|
-
ladok3-
|
|
18
|
-
ladok3-
|
|
19
|
-
ladok3-
|
|
20
|
-
ladok3-
|
|
21
|
-
ladok3-
|
|
17
|
+
ladok3-5.0.dist-info/LICENSE,sha256=Oe-mWTBQ-MzuA4jGuRDPDxQtMe8SuP_YjSQl7SOtZ7Q,1155
|
|
18
|
+
ladok3-5.0.dist-info/METADATA,sha256=8R5uIVTEFZfciQhdgCKklOxTAn6x2U7wD3LAPLCjGYs,9370
|
|
19
|
+
ladok3-5.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
20
|
+
ladok3-5.0.dist-info/entry_points.txt,sha256=pi-KKP5Obo0AyuDjXQUpadS9kIvAY2_5ORhPgEYlJv8,41
|
|
21
|
+
ladok3-5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|