Skip to content

Commit b6c38b6

Browse files
committed
Update automation 2025
Signed-off-by: Javier Ron Arteaga <[email protected]>
1 parent 1ebea3c commit b6c38b6

16 files changed

Lines changed: 949 additions & 6 deletions

.github/workflows/assignment-statistics.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
# Triggers the workflow on push or pull request events but only for the 2021 branch
88
push:
99
branches:
10-
- "2024"
10+
- "2025"
1111

1212
# Allows you to run this workflow manually from the Actions tab
1313
workflow_dispatch:

.github/workflows/check_task.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99
- edited # we need to check the new description against the README
1010
- synchronize
1111
branches:
12-
- 2024
12+
- 2025
1313
paths:
1414
- contributions/**
1515

.github/workflows/lecture_participation.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ permissions:
1010
jobs:
1111
track-participation:
1212
if: >
13-
github.event.issue.number == 2370 &&
13+
github.event.issue.number == 2697 &&
1414
github.event.comment.author_association != 'COLLABORATOR' &&
1515
github.event.comment.author_association != 'OWNER' &&
1616
github.event.comment.author_association != 'MEMBER'

.github/workflows/update_criteria.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ name: Grading criteria
66
on:
77
push:
88
branches:
9-
- 2024
9+
- 2025
1010
paths:
1111
- 'grading-criteria.md'
1212

.github/workflows/update_task.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ name: Update task
66
on:
77
push:
88
branches:
9-
- 2024
9+
- 2025
1010
paths:
1111
- contributions/**
1212

grading-criteria.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Grading Criteria of the KTH Devops Course
22

3-
- we are all aware that assessment may be subjective in nature. In case of disagreement, the informed judgment of the Professor is the final decision.
3+
- We are all aware that assessment may be subjective in nature. In case of disagreement, the informed judgment of the Professor is the final decision.
44
- Presentation and demos are mandatory.
55
- In case of a task failure, the students receive instructions for repetition through Canvas
66

tools/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
graders.json

tools/README.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Tools
2+
3+
## missing_grade.py
4+
This script is used to list the group who have not received a grade on canvas.
5+
6+
### Requirements
7+
8+
- Python 3
9+
- External modules
10+
- argparse
11+
- requests
12+
13+
### Usage
14+
15+
`python missing_grade.py --task executable-tutorial --deadline 1 --token 8779~...`
16+
17+
`python missing_grade.py --task presentation --week week2-testing-and-CI --token 88779~...`
18+
19+
| Option | Usage | Required | Note |
20+
|---|---|---|---|
21+
|--task| Task name you want to check | :heavy_check_mark:||
22+
|--token| Canvas access token generated on [on your profile](https://canvas.kth.se/profile/settings) | :heavy_check_mark:||
23+
|--week| Filter the by week folder |:x:| ONLY for presentation or demo |
24+
|--deadline| Filter the by task deadline |:x:| NOT for presentation or demo|
25+
26+
27+
`--week` and `--deadline` are optional, if you don't give them, you'll get all non graded groups for this task
28+
29+
## final_grade_exporter.py
30+
This script is used to grade each student according to the number of task completed
31+
32+
### Requirements
33+
34+
- Python 3
35+
- External modules
36+
- argparse
37+
- requests
38+
39+
### Update yearly
40+
- Lecture dates and start times in `/tools/track_participation_config.json`
41+
- Lecture participation issue number in `.github/lecture_participation.yml`
42+
43+
### Usage
44+
45+
`python3 final_grade_exporter.py --course XXXX --token 88779~...`
46+
47+
`python3 final_grade_exporter.py --course XXXX --token 88779~... --export grade.csv`
48+
49+
`python3 final_grade_exporter.py --course XXXX --token 88779~... --export grade.csv --fields kth_id grade`
50+
51+
| Option | Usage | Required | Default|
52+
|---|---|---|---|
53+
|--token| Canvas access token generated on [on your profile](https://canvas.kth.se/profile/settings) | :heavy_check_mark:||
54+
|--export| Path where to write the csv file |:x:| No writing |
55+
|--fields| Specify fields to write, *canvas_id name kth_id completed grade* |:x:| name kth_id grade|
56+
57+
58+
## stat_submission.py
59+
60+
### Requirements
61+
...
62+
63+
### Usage
64+
...
65+
66+
67+
## track_participation.py
68+
69+
Script used to track and display valid comments on lecture participation issue.
70+
Two files needs to be updated for each new year.
71+
1. Issue number in the [workflow file](https://github.com/KTH/devops-course/blob/d619654e15b89ebc504cf4a54ff119eaa8a9fdce/.github/workflows/lecture_participation.yml#L13).
72+
2. Lecture times [here](https://github.com/KTH/devops-course/blob/2024/tools/track_participation_config.json).
73+
74+
### Requirements
75+
76+
- Python 3
77+
- External modules
78+
- PTable
79+
- PyGithub
80+
81+
### Usage
82+
83+
Print the lecture participation in plaintext:
84+
85+
`python3 track_participation.py`
86+
87+
Print in markdown and update the issue:
88+
89+
`python3 track_participation.py ----printMarkdown --publish`
90+
91+
| Option | Usage | Required |
92+
|---|---|---|
93+
|--printMarkdown| Print participation stats in markdown |:x:|
94+
|--publish| Update the participation tracker issue |:x:|
95+
|--help | Displays help info |:x:|
96+

tools/final_grade_exporter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Script now at https://github.com/monperrus/programmable-teaching

tools/missing_grade.py

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
# Script to find assignments which have not been graded
4+
# Filename: missing_grade.py
5+
import json, os
6+
import argparse, requests
7+
import sys
8+
import zlib
9+
from datetime import datetime
10+
11+
TASK = ''
12+
WEEK = ''
13+
DEADLINE = 0
14+
CANVAS_TOKEN = ''
15+
CANVAS_URL = "https://canvas.kth.se"
16+
CANVAS_COURSE_ID = 48942 # 2024 edition
17+
CONTRIBUTION_PATH = '../contributions'
18+
19+
20+
# Mapping from github task name to canvas group set id
21+
def task_to_set(task_name, canvas_set):
22+
mapping = {
23+
#"course-automation": canvas_set["Course automation"],
24+
"demo": canvas_set["Demos"],
25+
"scientific-paper": canvas_set["Scientific Papers"],
26+
"executable-tutorial": canvas_set["Executable Tutorials"],
27+
"feedback": canvas_set["Feedback"],
28+
"opensource": canvas_set["Open-source contributions"],
29+
"presentation": canvas_set["Presentations"],
30+
}
31+
return mapping.get(task_name, Exception("Groupset mapping"))
32+
33+
34+
# Get all the group sets -> name : id
35+
def get_group_categories():
36+
url = "{0}/api/v1/courses/{1}/group_categories".format(CANVAS_URL, CANVAS_COURSE_ID)
37+
r = requests.get(url, headers={'Authorization': 'Bearer ' + CANVAS_TOKEN})
38+
if 'next' in r.links: raise Exception("need to implement paging")
39+
return {group["name"]: group["id"] for group in json.loads(r.content)}
40+
41+
42+
# Get assignments -> name : id
43+
def get_assignments():
44+
url = "{0}/api/v1/courses/{1}/assignments".format(CANVAS_URL, CANVAS_COURSE_ID)
45+
r = requests.get(url, headers={'Authorization': 'Bearer ' + CANVAS_TOKEN})
46+
if 'next' in r.links: raise Exception("need to implement paging")
47+
return {assignment["name"]: assignment["id"] for assignment in json.loads(r.content)}
48+
49+
50+
# Get groups in a group set -> name : id
51+
def list_groups(id_group_category):
52+
url = "{0}/api/v1/group_categories/{1}/groups?per_page=200".format(CANVAS_URL, id_group_category)
53+
r = requests.get(url, headers={'Authorization': 'Bearer ' + CANVAS_TOKEN})
54+
if 'next' in r.links: raise Exception("need to implement paging")
55+
return {group["name"]: group["id"] for group in json.loads(r.content)}
56+
57+
# Get groups members -> name : id
58+
def get_group_members(id_group):
59+
url = "{0}/api/v1/groups/{1}/users".format(CANVAS_URL, id_group)
60+
r = requests.get(url, headers={'Authorization': 'Bearer ' + CANVAS_TOKEN})
61+
if 'next' in r.links: raise Exception("need to implement paging")
62+
return {member["name"]: member["id"] for member in json.loads(r.content)}
63+
64+
65+
def grader(url):
66+
url = url.replace("/2024/","/2023/")
67+
graders = json.load(open("graders.json"))
68+
n= zlib.adler32(url.encode("utf-8"))%len(graders)
69+
return graders[n]
70+
raise Exception()
71+
72+
# Get groups in a group set
73+
def check_group_grading(groups, id_assignment):
74+
for group in groups:
75+
members = get_group_members(groups[group])
76+
for member in members:
77+
# doc: https://canvas.instructure.com/doc/api/assignments.html
78+
canvas_url = "{0}/api/v1/courses/{1}/assignments/{2}/submissions/{3}".format(CANVAS_URL, CANVAS_COURSE_ID,
79+
id_assignment,
80+
members[member])
81+
82+
# {'include[]': "submission_comments"} is a non-documented flag provided by colleague Chip Maguire
83+
r = requests.get(canvas_url, headers={'Authorization': 'Bearer ' + CANVAS_TOKEN}, params = {'include[]': "submission_comments"})
84+
if 'next' in r.links: raise Exception("need to implement paging")
85+
86+
comments = [x for x in r.json()["submission_comments"]]
87+
88+
last_comment = comments[-1]["comment"] if len(comments)>0 else ""
89+
90+
url = "https://github.com/KTH/devops-course/tree/"+datetime.today().strftime("%Y")+"/contributions/"+TASK+"/"+group
91+
92+
if "FAIL" in last_comment:
93+
print(url," failed by ",comments[-1]["author_name"])
94+
continue
95+
96+
if "FAIL" not in last_comment and json.loads(r.content)["entered_grade"] == "incomplete":
97+
print(url," REPEAT assigned to grader",grader(url))
98+
continue
99+
100+
graded = "graded" == json.loads(r.content)["workflow_state"]
101+
if not graded:
102+
#print(url)
103+
print(url," assigned to grader",grader(url))
104+
105+
#print("missing grade for", TASK, "of", group)
106+
break
107+
108+
109+
110+
# Get sub directories of a given path
111+
def get_sub_directory(path):
112+
categories = dict()
113+
114+
for dirpath, dirnames, filenames in os.walk(path, topdown=True):
115+
for category in dirnames:
116+
categories[category] = {"path": os.path.join(dirpath, category)}
117+
break
118+
119+
return categories
120+
121+
122+
# Parse arguments of the script
123+
def parse_args():
124+
global TASK
125+
global WEEK
126+
global DEADLINE
127+
global CANVAS_TOKEN
128+
129+
parser = argparse.ArgumentParser()
130+
parser.add_argument('--task', dest='task', type=str, help='Task name', required=True)
131+
parser.add_argument('--token', dest='token', type=str, help='Canvas access token', required=True)
132+
parser.add_argument('--week', dest='week', type=str, help='Week folder (ONLY for presentation/demo/scientific-paper)')
133+
parser.add_argument('--deadline', dest='deadline', type=int, help='Deadline number (NOT for presentation/demo/scientific-paper)')
134+
135+
args = parser.parse_args()
136+
TASK = args.task
137+
WEEK = args.week
138+
DEADLINE = args.deadline
139+
CANVAS_TOKEN = args.token
140+
141+
142+
# Keep only the groups for a given deadline number
143+
# For each group, we check the readme a look for "task x" if task is not present we keep the group
144+
def filter_deadline_groups(groups, deadline):
145+
sorted_groups = dict()
146+
for group in groups:
147+
f = open(groups[group]["path"] + '/README.md', "r")
148+
file = f.read().lower()
149+
150+
#if 'task ' not in file:
151+
#sorted_groups[group] = groups[group]
152+
#print("Not sure if the group " + group + " is in for this deadline, checking it anyway\n")
153+
if 'task ' + str(deadline) in file:
154+
sorted_groups[group] = groups[group]
155+
156+
return sorted_groups
157+
158+
def check_all_assigned():
159+
l = []
160+
#mapping = {
161+
#"course-automation": canvas_set["Course automation"],
162+
#"demo": canvas_set["Demos"],
163+
#"essay": canvas_set["Essays"],
164+
#"executable-tutorial": canvas_set["Executable Tutorials"],
165+
#"feedback": canvas_set["Feedback"],
166+
#"open-source": canvas_set["Open-source contributions"],
167+
#"presentation": canvas_set["Presentations"], }
168+
# for i in get_sub_directory(CONTRIBUTION_PATH+"/"+"course-automation").values():
169+
# l.append(i['path'])
170+
# for i in get_sub_directory(CONTRIBUTION_PATH+"/"+"essay").values():
171+
# l.append(i['path'])
172+
for i in get_sub_directory(CONTRIBUTION_PATH+"/"+"executable-tutorial").values():
173+
l.append(i['path'])
174+
for i in get_sub_directory(CONTRIBUTION_PATH+"/"+"feedback").values():
175+
l.append(i['path'])
176+
for i in get_sub_directory(CONTRIBUTION_PATH+"/"+"open-source").values():
177+
l.append(i['path'])
178+
179+
# already done
180+
#for i in get_sub_directory(CONTRIBUTION_PATH+"/"+"presentation").values():
181+
#for j in get_sub_directory(i['path']).values():
182+
#l.append(j['path'])
183+
#for i in get_sub_directory(CONTRIBUTION_PATH+"/"+"demo").values():
184+
#for j in get_sub_directory(i['path']).values():
185+
#l.append(j['path'])
186+
187+
# remove "../"
188+
l = ["https://github.com/KTH/devops-course/tree/"+datetime.today().strftime("%Y")+"/"+x[3:] for x in l]
189+
190+
191+
#print(l)
192+
193+
def main():
194+
parse_args()
195+
196+
if TASK == "check_all_assigned":
197+
check_all_assigned()
198+
return
199+
200+
canvas_groups_set = get_group_categories()
201+
canvas_groups_category_id = task_to_set(TASK, canvas_groups_set)
202+
canvas_groups = list_groups(canvas_groups_category_id)
203+
canvas_assignment = get_assignments()
204+
canvas_assignment_id = task_to_set(TASK, canvas_assignment)
205+
206+
task_sub = get_sub_directory(CONTRIBUTION_PATH + '/' + TASK)
207+
208+
# Filter github groups according to args
209+
if WEEK is not None:
210+
groups = get_sub_directory(task_sub[WEEK]["path"])
211+
print("Found " + str(len(groups)) + " group(s) for " + TASK + " in " + WEEK)
212+
elif DEADLINE is not None:
213+
groups = filter_deadline_groups(task_sub, str(DEADLINE))
214+
print(TASK + ", deadline " + str(DEADLINE))
215+
else:
216+
groups = canvas_groups
217+
print("Found " + str(len(groups)) + " group(s) for " + TASK)
218+
219+
# Intersection of canvas groups and github filtered groups
220+
canvas_groups = {x: canvas_groups[x] for x in canvas_groups if x in groups}
221+
222+
# Check the grading
223+
check_group_grading(canvas_groups, canvas_assignment_id)
224+
225+
226+
main()

0 commit comments

Comments
 (0)