GAS

Classroomの「生徒別連絡」を一括化する|スプレッドシート完結のGASツール【無料】

ウサギ

はじめに

こんにちは!ウサギです!

授業連絡でGoogle Classroomを用いている学校も多いと思います。

Classroomは授業の資料を配布したり、持ち物やテスト範囲を連絡したりなど便利ですよね。 また、個別の生徒に連絡する機能もあるため、欠席生徒への連絡や再テストのお知らせなどに活用することができます。

ただ、生徒によって伝えたい文言が違う場合や同じ連絡を複数クラスにまたがる生徒に個別連絡したい場合は、その都度投稿しないといけないため、地味に手間です。

本記事では、GASというシステムを用いて、個別連絡が簡単にできる仕組みづくりについて解説します。

ウサギ
ウサギ

GASって何?っていう人にもわかりやすく解説しますね!

以下でツールの配布をしているので、この記事を読んで導入すれば、個別連絡システムを今後使っていくことができますよ!

また、GASコードの全文を記事内で公開していますので、アレンジしたい人はご活用ください。

このツールでできること
  • ワンボタンで複数クラスの生徒にメッセージを個別送信する
  • メッセージの内容は生徒ごとに編集可能

前提:GASとは

GASとは Google Apps Script(グーグル・アップス・スクリプト) の略で、

Googleが無料で提供している 自動化・連携のための仕組み です。

といっても、難しく考える必要はありません。

簡単に言えば、

Googleスプレッドシート・Google Classroom・Gmail などを 「自動で」「まとめて」操作できるようにする道具

が GAS です。

GASでできることの例

GASを使うと、例えば次のようなことが可能になります。

  • ボタンを押すだけで
    → 毎回同じ作業を自動で実行する
  • スプレッドシートに書いた内容をもとに
    → Google Classroom に投稿したり
    → Googleカレンダーに予定登録をしたりする
  • Googleフォームに回答がきたら
    → その内容をClassroomに投稿する

つまり、

「人間が何度も同じ操作を繰り返している部分」を コンピュータに任せることができる のが GAS の強みです。

プログラミングができなくても大丈夫?

「スクリプト」「プログラミング」と聞くと、身構えてしまう方も多いと思います。

ですが、この記事で紹介するツールは

  • すでに必要なコードは書かれている
  • 基本操作はスプレッドシート上で完結
  • コードを理解しなくても使える

という設計になっています。

つまり、Excel(スプレッドシート)が操作できればOKというレベル感です。

使い方

まずは配布しているスプレッドシートをコピーして下さい。下記ボタンからコピーできます。

スプレッドシートをコピーしたら、以下の手順で実行して下さい。

上部メニューClassroom個別連絡→①コース一覧を更新をクリック

※上部にメニューがない場合は、スプレッドシートを一旦開き直して下さい。

認証設定をする

初めてプログラムを動作させるときには、認証設定が必要です。
以下の画面でOKを押して下さい。

Googleアカウントを確認すると、以下のような画面になりますので、「詳細」→「Classroom個別連絡(安全ではないページ)に移動」を選択して下さい。

以下の画面で、プログラムにClassroomを操作する権限を付与します。「すべて選択」をチェックして、下の方にある「続行」をクリックして下さい。

もう一度、上部メニューClassroom個別連絡→①コース一覧を更新をクリック

権限を付与したので、もう一度プログラムを実行しなおします。
無事実行できると以下のように自分が教員として参加しているコースが現れるはずです。

メッセージを送りたい生徒が所属しているコースを選択

コース一覧シートを選択して、メッセージを送りたい生徒が所属しているコース(クラス)を選択して下さい。
その後、上部メニューから「②生徒一覧を生成」をクリックします。
すると「送信設定」というシートに生徒一覧が生成されるはずです。

メッセージを編集して送信

送信設定タブを開いて、メッセージを編集します。メッセージを送る生徒をA列のチェック欄で選択して下さい。
その後、メッセージを編集して、上部メニューの送信ボタンを押すと、個別にメッセージが送信されるはずです。

以上の流れで、個別メッセージを送信することができます!

一度スプレッドシートを作っておけば、年度をまたいでずっと使い回すことができますので、ぜひお試し下さい!

自分で設定する方法(GASコード)

次に、手作業でスプレッドシートにプログラムを設置する方法を説明します。

スプレッドシートを作成する

Googleスプレッドシートを開いて空白のスプレッドシートを作成します。

GAS編集画面を開く

上部メニューから「拡張機能」→「Apps Script」を選択します。

コードを貼り付ける

コードの編集画面が現れます。
最初に書いてあるコードはすべて削除して、以下のコードを貼り付けて下さい。

貼り付けるコードは下記の通り(スクロールしてすべてコピーして下さい)

/**
 * ============================================================
 * Google Classroom 個別連絡ツール(Spreadsheet完結型)
 *
 * 【概要】
 * - Google Classroom の「生徒ごとにのみ表示されるお知らせ(Announcement)」を、
 *   スプレッドシート上でチェック&文章入力して一括送信するGASです。
 * - UIはHTML(サイドバー等)を使わず、スプレッドシートだけで完結します。
 *
 * 【基本フロー(メニュー操作)】
 * ① コース一覧を更新(アクティブなコースのみ)
 *    → 「コース一覧」シートにコースを一覧表示(チェックで選択)
 * ② 生徒一覧を生成
 *    → 選択したコースの生徒をまとめて取得し「送信設定」シートに一覧化
 * ③ 送信
 *    → 「送信設定」でチェックされた行だけ、生徒個別にAnnouncementを作成
 *
 * 【設計方針】
 * - 1人 = 1投稿(Classroomのストリームが汚れる点は許容)
 * - 差し込み変数等は使わず、確実性と簡単さを優先
 * - courseId / studentId はAPI呼び出しに必須だが、
 *   教員が触る必要はないのでシート上では列を非表示にしている
 *
 * 【事前準備(重要)】
 * - Apps Script の「サービス」で Google Classroom API(Classroom)を有効化してください。
 *   これが無いと `ReferenceError: Classroom is not defined` になります。
 * ============================================================
 */


/**
 * スプレッドシートを開いたときに、操作用メニューを追加します。
 */
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Classroom個別連絡')
    .addItem('①コース一覧を更新', 'refreshCourses')
    .addItem('②生徒一覧を生成', 'buildRecipients')
    .addSeparator()
    .addItem('③送信', 'sendFromSheet')
    .addToUi();
}


/**
 * ① コース一覧を取得して「コース一覧」シートに出力します。
 *
 * - teacherId: 'me' で「自分が教師のコース」に限定
 * - courseStates: ['ACTIVE'] で「アーカイブ済みを除外」
 * - APIはページングが必要なので nextPageToken を追って全件取得
 *
 * 出力列:
 * - 選択(チェックボックス)
 * - courseName(表示用)
 * - section(表示用)
 * - courseId(内部用:後続処理で必須だが列は非表示)
 */
function refreshCourses() {
  const ss = SpreadsheetApp.getActive();
  const sh = ss.getSheetByName('コース一覧') || ss.insertSheet('コース一覧');
  sh.clear();

  // ヘッダ行(1行目)
  sh.getRange(1, 1, 1, 4).setValues([['選択', 'courseName', 'section', 'courseId']]);

  const rows = [];
  let pageToken;

  // Classroom APIのページング処理
  do {
    const res = Classroom.Courses.list({
      teacherId: 'me',
      courseStates: ['ACTIVE'], // ★アクティブのみ(アーカイブ除外)
      pageSize: 100,
      pageToken
    });

    (res.courses || []).forEach(c => {
      rows.push([
        false,           // 選択(チェックボックス)
        c.name || '',    // courseName
        c.section || '', // section
        c.id             // courseId(内部用)
      ]);
    });

    pageToken = res.nextPageToken;
  } while (pageToken);

  // データ行を書き込み
  if (rows.length) {
    sh.getRange(2, 1, rows.length, 4).setValues(rows);
    sh.getRange(2, 1, rows.length, 1).insertCheckboxes();
  }

  // ざっくり体裁(詳細は formatClassListSheet_ で調整)
  sh.setFrozenRows(1);
  sh.autoResizeColumns(1, 3);

  // courseId は見えなくてよい(内部キー)
  sh.hideColumn(sh.getRange('D:D'));

  // 見た目・操作性を整える(罫線、背景、フィルタなど)
  formatClassListSheet_(sh);
}


/**
 * ② 「コース一覧」シートで選択されたコースの生徒を取得し、
 *   「送信設定」シートに統合一覧として出力します。
 *
 * 「送信設定」シートでやること:
 * - 送る生徒にチェック
 * - メッセージを入力(複数行想定)
 * - 送信後、status/error/announcementId が行ごとに記録される
 *
 * 出力列(9列):
 * A: 送信(チェックボックス)
 * B: courseName(表示用)
 * C: studentName(表示用)
 * D: message(手入力)
 * E: courseId(内部用:非表示)
 * F: studentId(内部用:非表示)
 * G: status(SUCCESS / FAIL)
 * H: error(失敗理由)
 * I: announcementId(成功時のID)
 */
function buildRecipients() {
  const ss = SpreadsheetApp.getActive();
  const classesSh = ss.getSheetByName('コース一覧');
  if (!classesSh) throw new Error('「コース一覧」シートがありません。先に①を実行してください。');

  // 「コース一覧」全体を読み込み(ヘッダ含む)
  const values = classesSh.getDataRange().getValues();

  // チェックされたコースのみ抽出
  const selected = values.slice(1)
    .filter(r => r[0] === true)
    .map(r => ({ courseName: r[1], section: r[2], courseId: r[3] }));

  if (selected.length === 0) throw new Error('「コース一覧」でコースを選択してください。');

  // 出力先(送信設定)
  const sh = ss.getSheetByName('送信設定') || ss.insertSheet('送信設定');
  sh.clear();

  // ヘッダ行
  sh.getRange(1, 1, 1, 9).setValues([[
    '送信', 'courseName', 'studentName', 'message',
    'courseId', 'studentId', 'status', 'error', 'announcementId'
  ]]);

  const rows = [];

  // 選択コースごとに生徒一覧を取得(ページングあり)
  for (const c of selected) {
    const courseDisplay = [c.courseName, c.section].filter(Boolean).join(' ');
    let pageToken;

    do {
      const res = Classroom.Courses.Students.list(c.courseId, {
        pageSize: 100,
        pageToken
      });

      (res.students || []).forEach(s => {
        const p = s.profile;
        rows.push([
          false,                    // 送信チェック
          courseDisplay,            // 表示用コース名
          p.name?.fullName || '(no name)', // 生徒名
          '',                       // メッセージ(手入力)
          c.courseId,               // 内部用 courseId
          p.id,                     // 内部用 studentId
          '',                       // status(送信後に書き込む)
          '',                       // error(送信後に書き込む)
          ''                        // announcementId(送信後に書き込む)
        ]);
      });

      pageToken = res.nextPageToken;
    } while (pageToken);
  }

  // 書き込み&チェックボックス化
  if (rows.length) {
    sh.getRange(2, 1, rows.length, 9).setValues(rows);
    sh.getRange(2, 1, rows.length, 1).insertCheckboxes();
  }

  // ざっくり体裁
  sh.setFrozenRows(1);
  sh.autoResizeColumns(1, 3);

  // 内部キー列は非表示(教員が触らないため)
  sh.hideColumn(sh.getRange('E:E')); // courseId
  sh.hideColumn(sh.getRange('F:F')); // studentId

  // 見た目・操作性(行高、折り返し、条件付き書式、フィルタなど)
  formatSendSettingsSheet_(sh, rows.length);
}


/**
 * ③ 「送信設定」シートのチェック行を送信します。
 *
 * - チェックされた行を抽出
 * - messageが空欄の行が混ざっていたら送信を中止(事故防止)
 * - 各行について1人1投稿でAnnouncementを作成
 * - 成功/失敗結果をシートに書き戻す
 *
 * ※ 「テスト送信」機能は今回は入れていません(要望により)
 */
function sendFromSheet() {
  const ss = SpreadsheetApp.getActive();
  const sh = ss.getSheetByName('送信設定');
  if (!sh) throw new Error('「送信設定」シートがありません。先に②を実行してください。');

  const values = sh.getDataRange().getValues();
  if (values.length <= 1) throw new Error('送信対象がありません。');

  // 送信対象を抽出(チェック = TRUE の行だけ)
  const items = [];
  for (let i = 1; i < values.length; i++) {
    const r = values[i];
    if (r[0] === true) {
      items.push({
        rowIndex: i + 1, // 書き戻し先(シート行番号)
        courseName: r[1],
        studentName: r[2],
        message: r[3],
        courseId: r[4],
        studentId: r[5]
      });
    }
  }

  if (items.length === 0) throw new Error('送信チェックが入っていません。');

  // 空メッセージ事故防止(チェックされているのに本文が空)
  const empties = items.filter(x => !x.message || !x.message.trim());
  if (empties.length > 0) {
    throw new Error(`メッセージ空欄の送信対象が ${empties.length} 件あります。`);
  }

  // バッチID(今回は表示のみ。ログシート等に保存する拡張も可能)
  const batchId = Utilities.getUuid();

  // 1人ずつ投稿(1人=1投稿)
  for (const item of items) {
    // 「その生徒だけに見える」Announcement
    const body = {
      text: item.message,
      assigneeMode: 'INDIVIDUAL_STUDENTS',
      individualStudentsOptions: { studentIds: [item.studentId] }
    };

    try {
      // レート制限・一時エラー対策でバックオフ付きにする
      const created = withBackoff_(() =>
        Classroom.Courses.Announcements.create(body, item.courseId)
      );

      // 成功時:結果を行に反映
      sh.getRange(item.rowIndex, 7).setValue('SUCCESS');       // status
      sh.getRange(item.rowIndex, 8).setValue('');              // error
      sh.getRange(item.rowIndex, 9).setValue(created.id || ''); // announcementId

    } catch (e) {
      // 失敗時:エラーを行に反映
      const msg = e?.message || String(e);
      sh.getRange(item.rowIndex, 7).setValue('FAIL');
      sh.getRange(item.rowIndex, 8).setValue(msg);
    }

    // APIを連打しすぎないための小休止(必要に応じて調整)
    Utilities.sleep(150);
  }

  SpreadsheetApp.getUi().alert(
    `送信完了(バッチID: ${batchId})\n結果は「送信設定」シートをご確認ください。`
  );
}


/**
 * レート制限(429)や一時的なサーバーエラー(500/503)などの際に、
 * 指数バックオフで再試行するためのヘルパー関数です。
 *
 * - maxRetries 回まで再試行
 * - wait(待機時間)は 300ms → 600ms → 1200ms … と増やす(上限5秒)
 *
 * 「確実に全部送る」ことより、「送信が途中で止まりにくい」ことを優先しています。
 */
function withBackoff_(fn) {
  const maxRetries = 5;
  let wait = 300;

  for (let i = 0; i <= maxRetries; i++) {
    try {
      return fn();
    } catch (e) {
      const msg = e?.message || String(e);

      // リトライしたいエラーだけ判定(雑に拾ってOK)
      const retryable =
        /429|RESOURCE_EXHAUSTED|Rate Limit|Service invoked too many times|500|503/i.test(msg);

      // リトライ対象外 or 最終回ならそのまま投げる
      if (!retryable || i === maxRetries) throw e;

      Utilities.sleep(wait);
      wait = Math.min(wait * 2, 5000);
    }
  }
}


/**
 * 「コース一覧」シートのUI整形
 * - 罫線、背景、列幅、フィルタなどを整える
 * - 2回目以降の実行でもエラーにならないよう、既存フィルタは削除してから作成
 */
function formatClassListSheet_(sh) {
  // ヘッダ固定&高さ
  sh.setFrozenRows(1);
  sh.setRowHeight(1, 28);

  // 列幅(操作性を優先)
  sh.setColumnWidth(1, 60);   // 選択
  sh.setColumnWidth(2, 320);  // courseName
  sh.setColumnWidth(3, 140);  // section
  // D列は非表示なので幅は気にしない

  // ヘッダ行の見た目
  const header = sh.getRange(1, 1, 1, 4);
  header.setFontWeight('bold')
        .setHorizontalAlignment('center')
        .setVerticalAlignment('middle')
        .setBackground('#f3f4f6');

  // データ範囲がある場合のみ整形
  const lastRow = sh.getLastRow();
  if (lastRow >= 2) {
    // データ行は縦中央
    const data = sh.getRange(2, 1, lastRow - 1, 3);
    data.setVerticalAlignment('middle');

    // 見やすい罫線(A〜C列)
    sh.getRange(1, 1, lastRow, 3).setBorder(true, true, true, true, true, true);

    // フィルタ(既存があれば削除)
    const existingFilter = sh.getFilter();
    if (existingFilter) existingFilter.remove();
    sh.getRange(1, 1, lastRow, 3).createFilter();
  }
}


/**
 * 「送信設定」シートのUI整形
 * - 200行程度でも編集しやすいよう、メッセージ列を広く・折り返し・上詰め
 * - 行の高さを確保(複数行メッセージを想定)
 * - status列にSUCCESS/FAILの色付け
 * - 2回目以降でもエラーにならないようフィルタは削除してから作成
 */
function formatSendSettingsSheet_(sh, rowCount) {
  // rowCount: データ行数(ヘッダ除く)
  sh.setFrozenRows(1);
  sh.setRowHeight(1, 30);

  // 列幅(重要)
  sh.setColumnWidth(1, 60);   // 送信
  sh.setColumnWidth(2, 220);  // courseName
  sh.setColumnWidth(3, 150);  // studentName
  sh.setColumnWidth(4, 520);  // message(主役)
  sh.setColumnWidth(7, 90);   // status
  sh.setColumnWidth(8, 260);  // error(長くなりがち)
  sh.setColumnWidth(9, 180);  // announcementId

  // ヘッダの見た目(濃色)
  const header = sh.getRange(1, 1, 1, 9);
  header.setFontWeight('bold')
        .setHorizontalAlignment('center')
        .setVerticalAlignment('middle')
        .setBackground('#111827')
        .setFontColor('#ffffff');

  // データが無ければここで終了
  if (rowCount <= 0) return;

  const startRow = 2;

  // 行の高さ:メッセージ欄を複数行で編集しやすくするため高め
  sh.setRowHeights(startRow, rowCount, 54);

  // メッセージ列:折り返し+上詰め(入力しやすい)
  const msgRange = sh.getRange(startRow, 4, rowCount, 1);
  msgRange.setWrap(true)
          .setVerticalAlignment('top');

  // 主要列の整列(見やすさ)
  sh.getRange(startRow, 1, rowCount, 3).setVerticalAlignment('middle');
  sh.getRange(startRow, 1, rowCount, 3).setHorizontalAlignment('center');
  sh.getRange(startRow, 2, rowCount, 1).setHorizontalAlignment('left'); // courseName
  sh.getRange(startRow, 3, rowCount, 1).setHorizontalAlignment('left'); // studentName

  // 表としての罫線
  sh.getRange(1, 1, rowCount + 1, 9).setBorder(true, true, true, true, true, true);

  // 背景(必要最低限:真っ白。交互色にしたい場合は拡張)
  sh.getRange(startRow, 1, rowCount, 9).setBackground('#ffffff');

  // フィルタ(既存があれば削除)
  const existingFilter = sh.getFilter();
  if (existingFilter) existingFilter.remove();
  sh.getRange(1, 1, rowCount + 1, 9).createFilter();

  // 条件付き書式(status列)
  const statusRange = sh.getRange(startRow, 7, rowCount, 1);
  const rules = [];

  // SUCCESS:薄い緑
  rules.push(
    SpreadsheetApp.newConditionalFormatRule()
      .whenTextEqualTo('SUCCESS')
      .setBackground('#dcfce7')
      .setRanges([statusRange])
      .build()
  );

  // FAIL:薄い赤
  rules.push(
    SpreadsheetApp.newConditionalFormatRule()
      .whenTextEqualTo('FAIL')
      .setBackground('#fee2e2')
      .setRanges([statusRange])
      .build()
  );

  // 既存ルールは置き換える
  sh.setConditionalFormatRules(rules);
}

Classroom APIを有効化する

GASからClassroomの機能を使うために、Google Classroom APIを有効化します。
左のサービスをクリックしてGoogle Classroom APIを探して追加して下さい。

プログラムを保存する

プログラムを保存したらGASの編集タブを閉じて大丈夫です。

スプレッドシートを開き直したら準備完了

一旦スプレッドシートを閉じて、もう一度開き直して下さい。
上部に「Classroom個別連絡」が表示されていたら設置成功です!

以上の準備を終わらせたら、先程説明した方法で実行できます。

まとめ

この記事では、Classroomへの個別投稿がワンボタンで実現できるスプレッドシートの配布と使い方の解説を行いました。

学校現場では、

  • 欠席した生徒への連絡
  • 課題の提出状況
  • 再テストの連絡
  • 面談日程の連絡
  • 部活動の連絡

など、複数クラスにまたがる生徒に、生徒に応じたメッセージを送りたい場面は意外とあります。

ぜひこのツールを使って、効率的な連絡方法を仕組み化してみてください♪

ABOUT ME
ウサギ
ウサギ
中高一貫校教員
中高一貫校で理科を教えています。 ICTを使った業務効率化を得意としています。
業務効率化と仕組みづくりを通して、教育という仕事を、もっと持続可能で夢のあるものにしたいと考えています。
最近はChatGPTに課金して毎日仕事の相棒として使っています。
記事URLをコピーしました