diff --git a/.github/workflows/lecture_participation.yml b/.github/workflows/lecture_participation.yml new file mode 100644 index 0000000000..601d190bfd --- /dev/null +++ b/.github/workflows/lecture_participation.yml @@ -0,0 +1,34 @@ +name: Lecture Participation Information + +on: + issue_comment: + types: [created] + +permissions: + issues: write + +jobs: + track-participation: + if: github.event.issue.number == 2370 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5.2.0 + with: + python-version: '3.11.8' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f ./tools/requirements.txt ]; then pip install -r ./tools/requirements.txt; fi + + - name: Update participation tracker + working-directory: tools + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO_FULLNAME: ${{ github.repository }} + TRACKER_ISSUE_NUMBER: ${{ github.event.issue.number }} + + run: python track_participation.py --printMarkdown --publish diff --git a/tools/README.md b/tools/README.md index d5092d656d..7bb7ec39f3 100644 --- a/tools/README.md +++ b/tools/README.md @@ -36,6 +36,10 @@ This script is used to grade each student according to the number of task comple - argparse - requests +### Update yearly +- Lecture dates and start times in `/tools/track_participation_config.json` +- Lecture participation issue number in `.github/lecture_participation.yml` + ### Usage `python3 final_grade_exporter.py --course XXXX --token 88779~...` @@ -58,3 +62,32 @@ This script is used to grade each student according to the number of task comple ### Usage ... + + +## track_participation.py + +Script used to track and display valid comments on lecture participation issue. + +### Requirements + +- Python 3 +- External modules + - PTable + - PyGithub + +### Usage + +Print the lecture participation in plaintext: + +`python3 track_participation.py` + +Print in markdown and update the issue: + +`python3 track_participation.py ----printMarkdown --publish` + +| Option | Usage | Required | +|---|---|---| +|--printMarkdown| Print participation stats in markdown |:x:| +|--publish| Update the participation tracker issue |:x:| +|--help | Displays help info |:x:| + diff --git a/tools/track_participation.py b/tools/track_participation.py new file mode 100644 index 0000000000..6241564337 --- /dev/null +++ b/tools/track_participation.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Filename: track_participation.py + +import sys +import os +import logging +import json +from github import Github, GithubException +import getopt +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo +from prettytable import PrettyTable + +PRINT_IN_MARKDOWN = False +PUBLISH = False + +GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") +REPO_FULLNAME = os.getenv("REPO_FULLNAME") +ISSUE_NUMBER = os.getenv("TRACKER_ISSUE_NUMBER") + +config = json.load(open("track_participation_config.json")) + +LECTURE_DATES_TO_NUMBER = config['LECTURE_DATES_TO_NUMBER'] +LECTURE_DATES_TO_START_TIME = config['LECTURE_DATES_TO_START_TIME'] +LECTURE_DURATION_HOURS = config['LECTURE_DURATION_HOURS'] +COMMENTING_LEEWAY_HOURS = config['COMMENTING_LEEWAY_HOURS'] +LECTURE_TIMEZONE = ZoneInfo(config['LECTURE_TIMEZONE']) + +commenting_duration_hours = LECTURE_DURATION_HOURS + COMMENTING_LEEWAY_HOURS + +def main(): + handle_args(sys.argv[1:]) + + repo, issue = get_repo_and_issue() + + participation = get_participation(issue) + + print_content = "" + + if PRINT_IN_MARKDOWN: + print_content = get_participation_markdown(participation) + else: + print_content = get_participation_text(participation) + + if PUBLISH: + update_issue_description(participation, issue) + print("Updated issue description\n") + + print(print_content) + + +def get_repo_and_issue(): + """ + Attempts to fetch the GitHub repository and issue using environment variables. + Exits on failure. + """ + try: + github = Github(GITHUB_TOKEN) + repo = github.get_repo(REPO_FULLNAME) + issue = repo.get_issue(number=int(ISSUE_NUMBER)) + return repo, issue + except GithubException as e: + logging.error(f"GitHub API error: {str(e)}") + sys.exit(1) + + +def get_participation(tracking_issue): + """ + Gets participation by checking each comment on the tracker issue. + Adds the lecture to the comment author if comment exists. Except if: + comment is from collaborator + comment is made outside of allowed time. + """ + participation = {} + + try: + collaborators = {collaborator.login for collaborator in tracking_issue.repository.get_collaborators()} + except GithubException as e: + logging.error(f"GitHub API error: {str(e)}") + sys.exit(1) + + for comment in tracking_issue.get_comments(): + author = comment.user.login + comment_time = comment.created_at.astimezone(LECTURE_TIMEZONE) + + # Ignore collaborators' comments + if author in collaborators: + continue + + if is_valid_lecture_time(comment_time): + lecture_date = comment_time.strftime("%Y-%m-%d") + if author not in participation: + participation[author] = {lecture_date} + else: + participation[author].add(lecture_date) + + return participation + + +def is_valid_lecture_time(comment_time): + """ + Checks if the comment was made on a valid day and time. + """ + lecture_date_str = comment_time.strftime("%Y-%m-%d") + + if lecture_date_str not in LECTURE_DATES_TO_START_TIME: + # comment not made on a lecture day + return False + + lecture_start_hour = LECTURE_DATES_TO_START_TIME[lecture_date_str] + + allowed_period_start = comment_time.replace(hour=lecture_start_hour, minute=0, second=0, microsecond=0) + allowed_period_end = allowed_period_start + timedelta(hours=commenting_duration_hours) + + # Check if the comment time falls within the valid window + return allowed_period_start <= comment_time <= allowed_period_end + + +def update_issue_description(participation, issue): + """ + Updates the repository issue description in markdown to reflect new participation. + """ + content = get_participation_markdown(participation) + try: + issue.edit(body=content) + except GithubException as e: + logging.error(f"GitHub API error: {str(e)}") + sys.exit(1) + + +def get_participation_markdown(participation): + """ + Returns markdown table representation of participation. + """ + current_time = datetime.now(LECTURE_TIMEZONE).strftime("%Y-%m-%d %H:%M:%S") + + content = f"Here we track active participation in lectures.\n\n" + content += ("To do this, you record as a comment the question you make to presentations or demos during the " + "lectures.\n\n") + content += "Also, provide the title of the presentation/demo.\n\n" + content += f"### Lecture Participation Stats (Updated on {current_time})\n\n" + content += "| Index | Student Name | Number of Lectures Attended | Lecture(s) attended |\n" + content += "|-------|--------------|-------------------|----------------|\n" + + index = 1 + for author, lectures in participation.items(): + lecture_numbers = [f"L{LECTURE_DATES_TO_NUMBER[lecture]}" for lecture in sorted(lectures)] + lectures_list = " ".join(map(str, lecture_numbers)) + total_lectures = len(lectures) + content += f"| {index} | {author} | {total_lectures} | {lectures_list} |\n" + index += 1 + + return content + + +def get_participation_text(participation): + """ + Returns plaintext table representation of participation. + """ + current_time = datetime.now(LECTURE_TIMEZONE).strftime("%Y-%m-%d %H:%M:%S") + + table = PrettyTable() + table.field_names = ["Index", "Student Name", "Number of Lectures Attended", "Lecture(s) attended"] + + index = 1 + for author, lectures in participation.items(): + lecture_numbers = [f"L{LECTURE_DATES_TO_NUMBER[lecture]}" for lecture in sorted(lectures)] + lectures_list = " ".join(map(str, lecture_numbers)) + total_lectures = len(lectures) + table.add_row([index, author, total_lectures, lectures_list]) + index += 1 + + return_str = f"Lecture Participation Stats (Updated on {current_time})\n\n" + return_str += table.get_string() + + return return_str + + +def handle_args(argv): + global PRINT_IN_MARKDOWN + global PUBLISH + + try: + opts, args = getopt.getopt(argv, "hm", ["help", "printMarkdown", "publish"]) + except getopt.GetoptError as error: + logging.error(error) + print_help_info() + sys.exit(2) + + for opt, _ in opts: + if opt == "--help": + print_help_info() + sys.exit() + elif opt == "--printMarkdown": + PRINT_IN_MARKDOWN = True + elif opt == "--publish": + PUBLISH = True + + +def print_help_info(): + print('') + print('DD2482 Student Lecture Participation Tracker Tool') + print('') + print('optional:') + print(' --printMarkdown Print participation in markdown syntax') + print(' --publish Update the participation tracker issue') + print('') + print('track_participation.py --help to display this help info') + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + if not GITHUB_TOKEN or not REPO_FULLNAME or not ISSUE_NUMBER: + logging.error("Required environment variables (GITHUB_TOKEN, REPO_FULLNAME, ISSUE_NUMBER) are missing") + sys.exit(1) + main() diff --git a/tools/track_participation_config.json b/tools/track_participation_config.json new file mode 100644 index 0000000000..2661c637d3 --- /dev/null +++ b/tools/track_participation_config.json @@ -0,0 +1,21 @@ +{ + "LECTURE_DATES_TO_NUMBER": { + "2024-09-04": 2, + "2024-09-11": 3, + "2024-09-18": 4, + "2024-09-25": 5, + "2024-10-02": 6, + "2024-10-09": 7 + }, + "LECTURE_DATES_TO_START_TIME": { + "2024-09-04": 13, + "2024-09-11": 13, + "2024-09-18": 13, + "2024-09-25": 13, + "2024-10-02": 13, + "2024-10-09": 13 + }, + "LECTURE_DURATION_HOURS": 4, + "COMMENTING_LEEWAY_HOURS": 1, + "LECTURE_TIMEZONE": "Europe/Stockholm" +}