Canvas 個別連絡 - eiichiromomma/CVMLAB GitHub Wiki

(Canvas) 個別連絡

Canvas API使ったら簡単に学生ごとに異なる内容のメッセージ送れた.(スクリプトは手抜きでLLMの支援で作成)

  1. Canvasの設定でアクセストークン生成
  2. APIで特定コースの受講者のUserID一覧を取得
  3. UserID, 表題(要コース名含), メッセージ なCSVを作る
  4. API使って送信

送信のときはコースの指定が無いので,何のコースについての連絡か表題に書かないと学生には意味不明になりそう.

アクセストークン

Canvasの自分のアカウントの設定画面の「承認済みのアプリケーション」で,「+新しいアクセストークン」ボタンを押して,区別のための名称と有効期限を設定してアクセストークンを記録する.

アクセストークンはパスワードと同じレベルで扱う

UserIDの取得

APIでのUser IDは評定表の出力時のIDな点に注意.SIS User IDでもSIS Login IDでもない.コースIDはCanvasでそのコースを開いたときにURLに含まれる数字5桁

import csv
import requests

CANVAS_DOMAIN = "https://CANVAS.DOMAIN/"
ACCESS_TOKEN = "YOUR_TOKEN"
COURSE_ID = "99999"

API_URL = f"{CANVAS_DOMAIN}/api/v1/courses/{COURSE_ID}/users"
HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}"}

params = { #メンバーのロール等での抽出はここで工夫する
    "enrollment_type[]": "student",
    "per_page": 100
}

users = []
url = API_URL

while url:
    r = requests.get(url, headers=HEADERS, params=params)
    r.raise_for_status()
    users.extend(r.json())

    # ページネーション処理
    url = None
    if "Link" in r.headers:
        links = r.headers["Link"].split(",")
        for link in links:
            if 'rel="next"' in link:
                url = link[link.find("<")+1:link.find(">")]
                break
    params = None  # next URLにはparams不要

# CSV出力
with open("course_users.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(["user_id", "name", "login_id"])
    for u in users:
        writer.writerow([u["id"], u["name"], u.get("login_id", "")])

print(f"{len(users)} users exported.")

messages.csv

dictで拾うのでヘッダは必須

user_id,subject,body
5桁ID,○○工学輪講について,あなたの論文はxxx.pdfです
5桁ID,○○工学輪講について,あなたの論文はyyy.pdfです

送信

コースIDの指定がなく,受講者へのメールのタイトルが「Teacher Name(Cource1およびCource2) Canvasであなたにメッセージを送りました」と付くようで.subjectに講義名重要.

import csv
import time
import requests

# ===== 設定 =====
CANVAS_DOMAIN = "https://CANVAS.DOMAIN/"
ACCESS_TOKEN = "YOUR_ACCESS_TOKEN"
CSV_FILE = "messages.csv"

API_URL = f"{CANVAS_DOMAIN}/api/v1/conversations"

HEADERS = {
    "Authorization": f"Bearer {ACCESS_TOKEN}"
}

# ===== 送信処理 =====
with open(CSV_FILE, newline='', encoding="utf-8") as f:
    reader = csv.DictReader(f)

    for i, row in enumerate(reader, start=1):
        user_id = row["user_id"]
        subject = row["subject"]
        body = row["body"]

        payload = {
            "recipients[]": user_id,
            "subject": subject,
            "body": body,
            "force_new": "true"   # スレッドを学生ごとに分離
        }

        response = requests.post(API_URL, headers=HEADERS, data=payload)

        if response.status_code == 201:
            print(f"[OK] {i}: user_id={user_id}")
        else:
            print(f"[NG] {i}: user_id={user_id} status={response.status_code}")
            print(response.text)

        # レート制限対策
        time.sleep(0.3)  # 200人ならこれでほぼ安全