From 6633ff18978d21dedf25f294dcc7d0375e34edb0 Mon Sep 17 00:00:00 2001 From: Aman Sharma Date: Fri, 28 Aug 2026 15:56:42 +0200 Subject: [PATCH 1/5] Replace presentation task with project; fix eager dict evaluation devops-course's 2026 offering replaced the "presentation" task category with "project" (see KTH/devops-course#2942). Updates the task/Canvas-group mapping and criteria validation to match. Also fixes task_to_group_category_id building its whole mapping dict eagerly regardless of the requested task_name: since it evaluated canvas_groups_set["Presentations"] unconditionally, checks for any other task (e.g. scientific-paper) would fail with a KeyError whenever the "Presentations" Canvas group category didn't exist -- which is expected now that it's not offered. It now only looks up the group for the requested task, and raises properly on an unmapped task name instead of silently returning an Exception object. --- update_grading.py | 2 +- update_task.py | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/update_grading.py b/update_grading.py index 5938891..19636f5 100644 --- a/update_grading.py +++ b/update_grading.py @@ -65,7 +65,7 @@ def parse_table(table): # Validation of the parsed criteria: Tasks, task items, tables items def validate_criteria(criteria): task = [ - "Presentations", + "Project", "Scientific Papers", "Demos", "Open-source contributions", diff --git a/update_task.py b/update_task.py index c9cbbec..2adb88a 100644 --- a/update_task.py +++ b/update_task.py @@ -116,15 +116,17 @@ def get_sections(path): # Mapping from github task name to canvas group set id def task_to_group_category_id(task_name, canvas_groups_set): - mapping = { - "presentation": canvas_groups_set["Presentations"], - "scientific-paper": canvas_groups_set["Scientific Papers"], - "demo": canvas_groups_set["Demos"], - "open-source": canvas_groups_set["Open-source contributions"], - "executable-tutorial": canvas_groups_set["Executable Tutorials"], - "feedback": canvas_groups_set["Feedback"] - } - return mapping.get(task_name, Exception("Groupset mapping")) + canvas_group_name = { + "project": "Project", + "scientific-paper": "Scientific Papers", + "demo": "Demos", + "open-source": "Open-source contributions", + "executable-tutorial": "Executable Tutorials", + "feedback": "Feedback" + }.get(task_name) + if canvas_group_name is None: + raise Exception("Groupset mapping: unknown task '{0}'".format(task_name)) + return canvas_groups_set[canvas_group_name] # Parse arguments of the script @@ -169,7 +171,7 @@ def main(): print("CANVAS_GROUPS_SET", canvas_groups_set) canvas_groups_category_id = task_to_group_category_id(task_name, canvas_groups_set) - if task_name == 'presentation' or task_name == 'demo' or task_name == 'scientific-paper': + if task_name == 'demo' or task_name == 'scientific-paper': weeks = get_sub_directory(github_tasks[task_name]["path"]) for week in weeks: if not week.startswith('week'): From ae6a3fce3291ccae2203327a6f62b300feb671dc Mon Sep 17 00:00:00 2001 From: Aman Sharma Date: Fri, 28 Aug 2026 17:27:31 +0200 Subject: [PATCH 2/5] Skip tasks without a fixed criteria table; fix task-name validation parse_criteria() crashed with IndexError on any section lacking the full title/description/table/grading-note structure -- e.g. "Project", which has no criteria table yet. It now skips such sections instead. validate_criteria()'s hardcoded task-name list also no longer matched the actual section headers (e.g. "Executable Tutorials" vs "Executable Tutorial", "Open-source contributions" vs "Open-Source Contribution"), so every check failed regardless. Corrected to match, and dropped "Project" from the required list since it's intentionally skipped for now. --- update_grading.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/update_grading.py b/update_grading.py index 19636f5..2c47694 100644 --- a/update_grading.py +++ b/update_grading.py @@ -35,10 +35,17 @@ def parse_criteria(): for section in sections[1:]: items = section.split("\n\n\n") - result[items[0].strip()] = {} - result[items[0].strip()]['description'] = items[1] - result[items[0].strip()]['table'] = parse_table(items[2]) - result[items[0].strip()]['grading'] = items[3] + name = items[0].strip() + if len(items) < 4: + # Task has no fixed criteria (title/description/table/grading) yet, + # e.g. a newly introduced task category -- skip syncing it rather + # than crash, until it's written up with the full structure. + print("Skipping '" + name + "': no criteria table yet") + continue + result[name] = {} + result[name]['description'] = items[1] + result[name]['table'] = parse_table(items[2]) + result[name]['grading'] = items[3] validate_criteria(result) return result @@ -64,12 +71,13 @@ def parse_table(table): # Validation of the parsed criteria: Tasks, task items, tables items def validate_criteria(criteria): + # "Project" is intentionally excluded: it has no fixed criteria table + # yet, so parse_criteria() skips it rather than including it here. task = [ - "Project", - "Scientific Papers", "Demos", - "Open-source contributions", - "Executable Tutorials", + "Scientific Papers", + "Executable Tutorial", + "Open-Source Contribution", "Feedback" ] task_items = [ From 5d6f1f27a29070d0e741bf2c1cf29941bcd1c025 Mon Sep 17 00:00:00 2001 From: Aman Sharma Date: Sat, 29 Aug 2026 11:22:58 +0200 Subject: [PATCH 3/5] Update parser/validator/rubric-builder for new grading-criteria.md schema grading-criteria.md was redesigned for 2026 (KTH/devops-course#2928) to use Category/Criterion/Description/Requirement table columns instead of the old Criteria/Yes/No checklist, which broke validate_criteria() (every row failed validation) and rubric_payload() (assumed 'Criteria' + arbitrary Yes/No rating columns). Category isn't used downstream, so it's dropped during parsing. Rubric mapping follows the shape of a prior year's real Canvas rubric: description is "Criterion: Description", and ratings are Mandatory(1pt)/-(0pt) for Mandatory rows or Yes(1pt)/No(0pt) otherwise. Also drops the auto-filed "grading file not formatted" GitHub issue on validation failure -- it hardcoded an assignee that's no longer a valid GitHub user, causing a 422 that masked the actual validation errors. --- update_grading.py | 15 +++------------ utils/course.py | 24 ++++++++++-------------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/update_grading.py b/update_grading.py index 2c47694..2a7fb0e 100644 --- a/update_grading.py +++ b/update_grading.py @@ -5,20 +5,15 @@ import os, sys, logging import argparse from utils.course import Course -from github import Github # ENVs for updating criteria CANVAS_TOKEN = os.getenv("CANVAS_TOKEN") CANVAS_COURSE_ID = os.getenv("CANVAS_COURSE_ID") -GH_TOKEN = os.getenv("GH_TOKEN") -GH_REPO_FULLNAME = os.getenv("GH_REPO_FULLNAME") CANVAS_URL = "https://canvas.kth.se" -github_repo = Github(GH_TOKEN).get_repo(GH_REPO_FULLNAME) course = Course(CANVAS_URL, CANVAS_TOKEN, CANVAS_COURSE_ID) # Arguments -ISSUE_ASSIGNEES = [''] GITHUB_GRADING_PATH = '' MODE = '' PR_NUMBER = 0 @@ -64,6 +59,8 @@ def parse_table(table): for col, value in zip(header, values): if col == '': col = 'Criteria' + if col == 'Category': + continue data[col] = value result.append(data) return result @@ -85,7 +82,7 @@ def validate_criteria(criteria): "table", "grading" ] - table_items = ["Criteria", "Yes", "No"] + table_items = ["Criterion", "Description", "Requirement"] errors = '' @@ -105,8 +102,6 @@ def validate_criteria(criteria): if errors: print(errors) - github_repo.create_issue("[CANVAS ACTION] Grading file is not correctly formatted", body=errors, - assignees=ISSUE_ASSIGNEES) raise Exception("The grading file is not correctly formatted ! ") @@ -166,22 +161,18 @@ def parse_args(): global GITHUB_GRADING_PATH global MODE global PR_NUMBER - global ISSUE_ASSIGNEES parser = argparse.ArgumentParser() parser.add_argument('--mode', dest='mode', type=str, help='Is only check') parser.add_argument('--pr', dest='pr', type=int, help='Pull request number', default=0) parser.add_argument('--grading', dest='grading_path', type=str, help='Path to the grading criteria', default='./grading-criteria.md') - parser.add_argument('--issue', dest='issue_assignee', type=str, nargs='+', help='List of issue assignee', - default=['']) args = parser.parse_args() GITHUB_GRADING_PATH = args.grading_path MODE = args.mode PR_NUMBER = args.pr - ISSUE_ASSIGNEES = args.issue_assignee def main(): diff --git a/utils/course.py b/utils/course.py index c41d1c2..296ef7e 100644 --- a/utils/course.py +++ b/utils/course.py @@ -18,24 +18,20 @@ def rubric_payload(id, name, description, criterias, assignment_id): } for i, criteria in enumerate(criterias): + if criteria['Requirement'] == 'Mandatory': + ratings = {'Mandatory': 1, '-': 0} + else: + ratings = {'Yes': 1, 'No': 0} + payload["rubric"]["criteria"][str(i + 1)] = { "points": 1, - "description": criteria['Criteria'], - "ratings": {} + "description": "{0}: {1}".format(criteria['Criterion'], criteria['Description']), + "ratings": { + str(j): {"description": label, "points": points} + for j, (label, points) in enumerate(ratings.items()) + } } - for j, option in enumerate(criteria): - - points = 0 - if criteria[option] == 'Yes': points = 1 - if criteria[option] == 'Mandatory': points = 1 - - if option != 'Criteria': - payload["rubric"]["criteria"][str(i + 1)]["ratings"][str(j)] = { - "description": criteria[option], - "points": points, - } - return payload From d833bef7ec010c8adb5d213c5087912d3fff7a57 Mon Sep 17 00:00:00 2001 From: Aman Sharma Date: Mon, 31 Aug 2026 13:59:50 +0200 Subject: [PATCH 4/5] Stop excluding Project from Canvas grading sync Project now ships with a full criteria table (KTH/devops-course#2955), so it no longer needs the skip-workaround that was added while it had none. Relies on the matching grading-criteria.md whitespace fix so every section (including Project) splits into exactly 4 parts. --- update_grading.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/update_grading.py b/update_grading.py index 2a7fb0e..4379075 100644 --- a/update_grading.py +++ b/update_grading.py @@ -31,12 +31,6 @@ def parse_criteria(): for section in sections[1:]: items = section.split("\n\n\n") name = items[0].strip() - if len(items) < 4: - # Task has no fixed criteria (title/description/table/grading) yet, - # e.g. a newly introduced task category -- skip syncing it rather - # than crash, until it's written up with the full structure. - print("Skipping '" + name + "': no criteria table yet") - continue result[name] = {} result[name]['description'] = items[1] result[name]['table'] = parse_table(items[2]) @@ -68,9 +62,8 @@ def parse_table(table): # Validation of the parsed criteria: Tasks, task items, tables items def validate_criteria(criteria): - # "Project" is intentionally excluded: it has no fixed criteria table - # yet, so parse_criteria() skips it rather than including it here. task = [ + "Project", "Demos", "Scientific Papers", "Executable Tutorial", From 50feba88283fd10424cb60a1a59e72a416818d82 Mon Sep 17 00:00:00 2001 From: Jiaxun Wei Date: Thu, 3 Sep 2026 09:15:10 +0200 Subject: [PATCH 5/5] Align task group-category names with Canvas for update_task.py (#1) --- update_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/update_task.py b/update_task.py index 2adb88a..5fb0bfd 100644 --- a/update_task.py +++ b/update_task.py @@ -120,8 +120,8 @@ def task_to_group_category_id(task_name, canvas_groups_set): "project": "Project", "scientific-paper": "Scientific Papers", "demo": "Demos", - "open-source": "Open-source contributions", - "executable-tutorial": "Executable Tutorials", + "open-source": "Open-Source Contribution", + "executable-tutorial": "Executable Tutorial", "feedback": "Feedback" }.get(task_name) if canvas_group_name is None: