ladok3 4.13__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.
ladok3/scripts.nw ADDED
@@ -0,0 +1,244 @@
1
+ In this chapter we'll see some scripts for reporting results from Canvas to
2
+ LADOK.
3
+ These scripts are run as cronjobs.
4
+ This means that they run unattended and only produce output when they need
5
+ attention.
6
+
7
+ We want to report results for courses that are titled \enquote{DD2520 VT25},
8
+ \enquote{DD1310 HT24}, \enquote{DD1317 HT24}, and similar in Canvas.
9
+ The advantage to using this command is that it will automatically report the
10
+ correct dates and everyone who has participated in the grading of each
11
+ student---as required by regulation.
12
+ The official tools, like KTH Transfer to Ladok or SUNET's version of the same,
13
+ don't do this.
14
+ They don't set the dates correctly, meaning that each individual should have a
15
+ separate date (date of submission).
16
+ They also don't register the graders in LADOK.
17
+ For each results, everyone who participated in the grading process should be
18
+ registered in LADOK.
19
+
20
+ We'll have a script [[<<ladok.sh>>]] that is run by [[cron]].
21
+ It's useful to load our profile, so that we have our normal environment.
22
+ <<ladok.sh>>=
23
+ #!/bin/bash
24
+ . ${HOME}/.profile
25
+ @
26
+
27
+ We also want some helper functions and a main function that is only run when
28
+ the script is run.
29
+ If the script is sourced, the main function is not run.
30
+ This way we can use the helper functions in our terminal.
31
+ <<ladok.sh>>=
32
+ <<constants>>
33
+ <<helper functions>>
34
+
35
+ main() {
36
+ <<main script>>
37
+ }
38
+
39
+ # Only run if this is the main script
40
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
41
+ main "$@"
42
+ fi
43
+ @
44
+
45
+ We need to report more than once for a course.
46
+ All students might not be done by the time of reporting.
47
+ When they're done, we must report the results again\footnote{%
48
+ And students have a right to do this without having to reregister on a later
49
+ course round.
50
+ }.
51
+ Sometimes a student finishes a course years after it ended.
52
+ To speed up execution it's better to report results for a sliding window of
53
+ courses, instead of all courses.
54
+ For those rare cases when a student finishes a course years after it ended, we
55
+ can report the results manually for that course again---by importing and
56
+ invoking the functions below in the terminal.
57
+
58
+ We'll let [[YEARS]] be a regex for the years that we're interested in reporting
59
+ for.
60
+ We'll use the current year and the previous year.
61
+ The advantage of this window is that when we pass new year's eve, the autumn
62
+ courses are still current for a few weeks---but the year is the wrong one.
63
+ As a side effect, during autumn we report results for any late results for the
64
+ previous year too.
65
+
66
+ To construct the regex for years, we simply take a sequence of years
67
+ ([[24 25]]) and make a regex ([[(24|25)]]) out of it.
68
+ <<constants>>=
69
+ YEAR=$(date +%y)
70
+ YEARS=$(echo -n "("; \
71
+ seq $((YEAR - 1)) $YEAR \
72
+ | sed -zE "s/\s/|/g" \
73
+ | sed "s/|$/)/")
74
+ @
75
+
76
+ \section{Reporting results on course components}
77
+
78
+ We want a script that reports the results for all courses, including all
79
+ previous years, to LADOK.
80
+ We want to do this for previous years as sometimes they finish assignments
81
+ years after the course ended.
82
+
83
+ We'll add a function that takes a course regex and a component regex and
84
+ reports the results to LADOK.
85
+ To report the modules, we sometimes need to override the default summary
86
+ function of [[canvaslms]]\footnote{\label{canvaslms-doc}%
87
+ For details on [[canvaslms results]], see Chapters 10 and 11 in its
88
+ documentation, found on
89
+ \url{https://github.com/dbosk/canvaslms/releases/tag/v4.7}.
90
+ Particularly Chapter 11 discusses the summary modules.
91
+ You can also read [[pydoc canvaslms.grades]] for a more brief summary.
92
+ }.
93
+ We want something like this:
94
+ <<main script>>=
95
+ report_components "DD1301 HT${YEARS}" \
96
+ LAB1
97
+ report_components "DD131[057] HT${YEARS}" \
98
+ "(LAB|MAT|KAL)[1-3]"
99
+ report_components "DA2215 [HV]T${YEARS}" \
100
+ INL1
101
+
102
+ report_components "DD2520 VT${YEARS}" \
103
+ INL1
104
+ report_components "DD2520 VT${YEARS}" \
105
+ LAB1 \
106
+ canvaslms.grades.tilkryLAB1
107
+ @ Note that the line reporting for DD1310 reports for \emph{all} instances as
108
+ well---that course is given five times in parallel.
109
+ But the assessment should be the same so it is sufficient that one of the
110
+ examiners run this script to report all the results.
111
+
112
+ To get the results out of Canvas we'll use the [[canvaslms results]] command.
113
+ We must install the [[canvaslms]] tool.
114
+ We can do this by running:
115
+ \begin{minted}{bash}
116
+ python3 -m pip install canvaslms # to use it with Python, or
117
+ pipx install canvaslms # to only use the CLI
118
+ canvaslms login # or read `canvaslms login --help`
119
+ \end{minted}
120
+ For details on how to extract the results, read
121
+ \mintinline{bash}{canvaslms results --help}
122
+ (also read \cref{canvaslms-doc} on page~\pageref{canvaslms-doc}).
123
+ <<helper functions>>=
124
+ report_components() {
125
+ local course="$1"
126
+ local component="$2"
127
+ local summary_module="$3"
128
+
129
+ local summary_opt=""
130
+ if [[ -n "$summary_module" ]]; then
131
+ summary_opt="-S $summary_module"
132
+ fi
133
+
134
+ # Get the course results from Canvas.
135
+ canvaslms results -c "$course" -A "$component" $summary_opt \
136
+ | sed -E "s/ ?[HV]T[0-9]{2}( \(.*\))?//" \
137
+ | <<[[tee]] the component results to use for course grades>> \
138
+ | ladok report -fv # Report them to LADOK.
139
+ }
140
+ @
141
+
142
+
143
+ \section{Reporting course grade}
144
+
145
+ Now we can set the course grades based on the reported components.
146
+ We'll have to report one course at a time since the course grade is based on
147
+ different components in different courses.
148
+ <<main script>>=
149
+ report_course "DD131[057] HT${YEARS}" \
150
+ LAB3
151
+ report_course "DD2520 VT${YEARS}" \
152
+ LAB1
153
+ @ We don't need to report the course grade for the other courses since they
154
+ have only one component.
155
+ For courses with only one component, LADOK will automatically set the course
156
+ grade based on the grade of the single component.
157
+
158
+ Setting the course grade can be done in several ways.
159
+ The first option is to look at what results are already attested in LADOK.
160
+ Unfortunately, this requires the round code---which we don't have access
161
+ to\footnote{%
162
+ For a brief period, IT included the round code in the course title in Canvas
163
+ at KTH.
164
+ That was beneficial in many ways, but unfortunately it faced a backlash from
165
+ teachers and it was undone.
166
+ }.
167
+ However, we can get this data from the [[canvaslms results]] line in
168
+ [[report_components]].
169
+ That's why we want to [[tee]] that data out of that pipeline.
170
+
171
+ When we [[tee]] the data out, we want to use the [[-a]] option to append if the
172
+ file already exists.
173
+ The reason for this is that we want all results for the course in one file.
174
+ But sometimes we might have to run the script several times---once for each
175
+ component.
176
+ <<[[tee]] the component results to use for course grades>>=
177
+ tee -a "${DATA_DIR}/${course}-results.csv"
178
+ <<constants>>=
179
+ DATA_DIR=`mktemp -d`
180
+ @
181
+
182
+ We'll provide a function that takes a course and a component and returns the
183
+ results for that course and component.
184
+ If no component is given, we use all components.
185
+ If we don't have results from before, we report those components to get the
186
+ data.
187
+ <<helper functions>>=
188
+ component_grades() {
189
+ local course="$1"
190
+ local component="${2:-[A-Z]{3}[0-9]+}"
191
+ local grades="${DATA_DIR}/${course}-results.csv"
192
+
193
+ if [[ ! -f "$grades" ]]; then
194
+ report_components "$course" "$component"
195
+ fi
196
+ cat "$grades" \
197
+ | grep -E "\s${component}\s" \
198
+ | sort -u
199
+ }
200
+ @ The data we get here has the following columns (tab separated):
201
+ \begin{minted}{text}
202
+ course component student grade date graders
203
+ \end{minted}
204
+
205
+ Now we can use this file when reporting the course grades.
206
+ If the file doesn't exist, we simply run [[report_components]].
207
+ If the results are not yet attested (certified), the [[ladok report]] command
208
+ will simply give an error that all components of the course are not yet
209
+ attested.
210
+
211
+ Now it's just to sort out the students and then for each student get the grade
212
+ of the component, get the latest grade date of all components and finally
213
+ report to LADOK.
214
+ <<helper functions>>=
215
+ report_course() {
216
+ local course="$1"
217
+ local component="$2"
218
+
219
+ for student in $(component_grades "$course" \
220
+ | cut -f 3)
221
+ do
222
+ local grade=$(component_grades "$course" "$component" \
223
+ | grep "$student" \
224
+ | cut -f 4)
225
+ local grade_date=$(component_grades "$course" \
226
+ | grep "$student" \
227
+ | cut -f 5 \
228
+ | sort \
229
+ | tail -n 1)
230
+
231
+ if [ "$grade" = "" ]; then
232
+ continue
233
+ fi
234
+
235
+ local course_code=$(component_grades "$course" "$component" \
236
+ | grep "$student" \
237
+ | cut -f 1 \
238
+ | sort -u)
239
+
240
+ # `component code = course code` yields final grade on course.
241
+ ladok report -fv "$course_code" "$course_code" \
242
+ "$student" "$grade" "$grade_date"
243
+ done
244
+ }
ladok3/student.nw CHANGED
@@ -23,6 +23,14 @@ import ladok3.cli
23
23
  <<functions>>
24
24
 
25
25
  def add_command_options(parser):
26
+ """Add the 'student' subcommand options to the argument parser.
27
+
28
+ Creates a subparser for displaying student information with options
29
+ for course filtering and result display.
30
+
31
+ Args:
32
+ parser (ArgumentParser): The parent parser to add the subcommand to.
33
+ """
26
34
  student_parser = parser.add_parser("student",
27
35
  help="Shows a student's information in LADOK",
28
36
  description="""
@@ -34,6 +42,12 @@ def add_command_options(parser):
34
42
  <<add student command arguments to student parser>>
35
43
 
36
44
  def command(ladok, args):
45
+ """Execute the student information display command.
46
+
47
+ Args:
48
+ ladok (LadokSession): The LADOK session for data access.
49
+ args: Parsed command line arguments containing student ID and display options.
50
+ """
37
51
  <<print info depending on args>>
38
52
  @
39
53
 
@@ -74,6 +88,17 @@ student_parser.add_argument("-r", "--results",
74
88
  )
75
89
  @
76
90
 
91
+ \subsection{A contact information flag}
92
+
93
+ We also want a flag for specifying whether or not to include the student's
94
+ contact information (email, phone, address).
95
+ <<add student command arguments to student parser>>=
96
+ student_parser.add_argument("--contact",
97
+ action="store_true", default=False,
98
+ help="Include contact information (email, phone, address)."
99
+ )
100
+ @
101
+
77
102
  \section{Print the student data}
78
103
 
79
104
  Now that we have the student identifier, we can simply use that to fetch the
@@ -87,7 +112,7 @@ try:
87
112
  except Exception as err:
88
113
  ladok3.cli.err(-1, f"can't fetch student data for {args.id}: {err}")
89
114
 
90
- print_student_data(student)
115
+ print_student_data(student, args)
91
116
 
92
117
  if args.course:
93
118
  print()
@@ -99,16 +124,56 @@ if args.course:
99
124
  To print the student's personal data, we simply print the most interesting
100
125
  attributes.
101
126
  <<functions>>=
102
- def print_student_data(student):
103
- """Prints the student data, all attributes, to stdout."""
127
+ def print_student_data(student, args):
128
+ """Prints the student data, all attributes, to stdout.
129
+
130
+ Args:
131
+ student: A Student object with the student's data
132
+ args: Command line arguments, including flags like --contact
133
+ """
104
134
  print(f"First name: {student.first_name}")
105
135
  print(f"Last name: {student.last_name}")
106
136
  print(f"Personnummer: {student.personnummer}")
107
137
  print(f"LADOK ID: {student.ladok_id}")
108
138
  print(f"Alive: {student.alive}")
109
- print(f"Suspended: {student.suspensions}")
139
+ <<print info about suspensions for [[student]]>>
140
+ <<print contact info if requested>>
110
141
  @
111
142
 
143
+ We want to print whether the student is currently suspended or not.
144
+ We want to make this really clear.
145
+ <<print info about suspensions for [[student]]>>=
146
+ print(f"Suspended: ", end="")
147
+ if any(map(lambda x: x.is_current, student.suspensions)):
148
+ print("YES")
149
+ else:
150
+ print("no")
151
+ @ Then we also want to print all the times the student has been suspended.
152
+ We only want to print this if the student has been suspended at least once.
153
+ <<print info about suspensions for [[student]]>>=
154
+ if student.suspensions:
155
+ print(f"Suspenions: ", end="")
156
+ for suspension in student.suspensions:
157
+ print(f"{suspension}", end="\n ")
158
+ print()
159
+ @
160
+
161
+ Now we add the contact information display.
162
+ We only want to print this if the user has requested it using the [[--contact]] flag.
163
+ <<print contact info if requested>>=
164
+ if args.contact:
165
+ print("Contact information:")
166
+ if student.email:
167
+ print(f"Email: {student.email}")
168
+ if student.phone:
169
+ print(f"Phone: {student.phone}")
170
+ if student.address:
171
+ print(f"Address: {student.address[0]}")
172
+ for line in student.address[1:]:
173
+ print(f" {line}")
174
+ @
175
+
176
+
112
177
  \subsection{Printing student's course data}
113
178
 
114
179
  To print the student's course data, we simply filter the courses on the option
ladok3/student.py CHANGED
@@ -1,54 +1,110 @@
1
1
  import csv
2
2
  import ladok3.cli
3
3
 
4
- def print_student_data(student):
5
- """Prints the student data, all attributes, to stdout."""
6
- print(f"First name: {student.first_name}")
7
- print(f"Last name: {student.last_name}")
8
- print(f"Personnummer: {student.personnummer}")
9
- print(f"LADOK ID: {student.ladok_id}")
10
- print(f"Alive: {student.alive}")
11
- print(f"Suspended: {student.suspensions}")
4
+
5
+ def print_student_data(student, args):
6
+ """Prints the student data, all attributes, to stdout.
7
+
8
+ Args:
9
+ student: A Student object with the student's data
10
+ args: Command line arguments, including flags like --contact
11
+ """
12
+ print(f"First name: {student.first_name}")
13
+ print(f"Last name: {student.last_name}")
14
+ print(f"Personnummer: {student.personnummer}")
15
+ print(f"LADOK ID: {student.ladok_id}")
16
+ print(f"Alive: {student.alive}")
17
+ print(f"Suspended: ", end="")
18
+ if any(map(lambda x: x.is_current, student.suspensions)):
19
+ print("YES")
20
+ else:
21
+ print("no")
22
+ if student.suspensions:
23
+ print(f"Suspenions: ", end="")
24
+ for suspension in student.suspensions:
25
+ print(f"{suspension}", end="\n ")
26
+ print()
27
+ if args.contact:
28
+ print("Contact information:")
29
+ if student.email:
30
+ print(f"Email: {student.email}")
31
+ if student.phone:
32
+ print(f"Phone: {student.phone}")
33
+ if student.address:
34
+ print(f"Address: {student.address[0]}")
35
+ for line in student.address[1:]:
36
+ print(f" {line}")
37
+
38
+
12
39
  def print_course_data(student, args):
13
- """Prints the courses"""
14
- print("Courses:")
15
- for course in student.courses(code=args.course):
16
- print(f"{course}")
17
- if args.results:
18
- for result in course.results():
19
- print(f" {result}")
40
+ """Prints the courses"""
41
+ print("Courses:")
42
+ for course in student.courses(code=args.course):
43
+ print(f"{course}")
44
+ if args.results:
45
+ for result in course.results():
46
+ print(f" {result}")
47
+
20
48
 
21
49
  def add_command_options(parser):
22
- student_parser = parser.add_parser("student",
23
- help="Shows a student's information in LADOK",
24
- description="""
50
+ """Add the 'student' subcommand options to the argument parser.
51
+
52
+ Creates a subparser for displaying student information with options
53
+ for course filtering and result display.
54
+
55
+ Args:
56
+ parser (ArgumentParser): The parent parser to add the subcommand to.
57
+ """
58
+ student_parser = parser.add_parser(
59
+ "student",
60
+ help="Shows a student's information in LADOK",
61
+ description="""
25
62
  Show a student's information in LADOK.
26
63
  Shows information like name, personnummer, contact information.
27
- """
28
- )
29
- student_parser.set_defaults(func=command)
30
- student_parser.add_argument("id",
31
- help="The student's ID, either personnummer or LADOK ID"
32
- )
33
- student_parser.add_argument("-c", "--course",
34
- nargs="?", const=".*",
35
- help="A regular expression for which course codes to list, " \
36
- "use no value for listing all courses."
37
- )
38
- student_parser.add_argument("-r", "--results",
39
- action="store_true", default=False,
40
- help="Set to include results for each course listed."
41
- )
64
+ """,
65
+ )
66
+ student_parser.set_defaults(func=command)
67
+ student_parser.add_argument(
68
+ "id", help="The student's ID, either personnummer or LADOK ID"
69
+ )
70
+ student_parser.add_argument(
71
+ "-c",
72
+ "--course",
73
+ nargs="?",
74
+ const=".*",
75
+ help="A regular expression for which course codes to list, "
76
+ "use no value for listing all courses.",
77
+ )
78
+ student_parser.add_argument(
79
+ "-r",
80
+ "--results",
81
+ action="store_true",
82
+ default=False,
83
+ help="Set to include results for each course listed.",
84
+ )
85
+ student_parser.add_argument(
86
+ "--contact",
87
+ action="store_true",
88
+ default=False,
89
+ help="Include contact information (email, phone, address).",
90
+ )
91
+
42
92
 
43
93
  def command(ladok, args):
44
- try:
45
- student = ladok.get_student(args.id)
46
- student.alive
47
- except Exception as err:
48
- ladok3.cli.err(-1, f"can't fetch student data for {args.id}: {err}")
94
+ """Execute the student information display command.
95
+
96
+ Args:
97
+ ladok (LadokSession): The LADOK session for data access.
98
+ args: Parsed command line arguments containing student ID and display options.
99
+ """
100
+ try:
101
+ student = ladok.get_student(args.id)
102
+ student.alive
103
+ except Exception as err:
104
+ ladok3.cli.err(-1, f"can't fetch student data for {args.id}: {err}")
49
105
 
50
- print_student_data(student)
106
+ print_student_data(student, args)
51
107
 
52
- if args.course:
53
- print()
54
- print_course_data(student, args)
108
+ if args.course:
109
+ print()
110
+ print_course_data(student, args)