見出し画像

【コード解説Ver.2】Classroomを一括作成し、管理職や教師を自動招待するWebアプリ|GAS【教員向け】

今回の「コード解説」シリーズでは、Google Apps Scriptを使って作成した、Google Classroomの一括作成、学校用アカウントのオーナー設定、管理職や教師の自動招待をまとめて行えるWebアプリのコード構成と処理内容を解説します。

このツールでは、複数のClassroomをまとめて作成するとともに、次の処理を自動で行います。

  • 学校用アカウントをClassroomのオーナーに設定

  • 管理職や担当者を教師として常時招待

  • Webアプリを実行したユーザーを教師として招待

  • 必要に応じて追加の教師アカウントを招待

  • 学校名をClassroomのセクション欄に設定

  • 作成結果やエラー内容をスプレッドシートへ記録


目次



ツールの導入方法と配布先

ツールの導入方法や実際の使い方については、こちらをご覧ください。

解説動画

ツール配布先


コード解説動画

文章とコードを見ながら確認したい場合は本記事を、
処理の流れを映像で確認したい場合は動画をご利用ください。


主な更新点

  1. 一度に作成できるClassroom数の上限を追加

  2. 設定情報をまとめて取得し、シートへのアクセス回数を削減

  3. メールアドレスの正規化、形式確認、重複除外を追加

  4. オーナーを教師招待の対象から除外

  5. Webアプリの画面デザインと結果表示を改善

  6. ClassroomとGmailを確認するボタンを追加


ファイル構成

今回のツールでは、目的や処理内容に応じて、コードを7つのファイルに分けています。

  • Config.gs

    • シート名や行番号
      一度に作成できるClassroom数などの共通設定

  • WebApp.gs

    • Webアプリの初期画面を返す処理

  • Utils.gs

    • 入力文字列の分割やメールアドレス形式の確認
      シートの取得などの共通処理

  • Account.gs

    • 設定シートから学校名、オーナー、実行者、
      常時招待するアカウントの情報を取得し、検証や整形を行う処理

  • Classroom.gs

    • 入力内容の検証、Classroomの作成、教師の招待、
      HTMLへ返す結果データの作成

  • Log.gs

    • Classroomの作成履歴をLogシートへ記録

  • index.html

    • Webアプリの画面、入力操作、GAS側の関数呼び出し、
      作成結果や教師招待結果の表示処理


処理全体の流れ

WebアプリからClassroomを作成するまでの処理は、次の7段階です。

  1. Webアプリを開く

  2. 設定シートから学校名やオーナー情報を取得

  3. 作成するClassroom名などの入力

  4. HTMLからGASの関数を呼び出す

  5. Classroomを1件ずつ作成し教師を招待する

  6. ログを記録

  7. Webアプリへ結果を返す

このツールでは、

  • スプレッドシート

  • GAS

  • HTML

  • Classroom API

の4つが連携して動作します。

起動時の処理の流れ

  1. WebApp.gsからindex.htmlが読み込まれる
    Webアプリが表示される

  2. Account.gsのgetConfigSettings()から
    設定情報が読み込まれて表示される

Classroom作成時の処理の流れ

  1. HTML側からClassroom.gsにある
    createClassroomsFromHtml()を実行

  2. Classroom.gsの内部関数
    createOneClassroom_()でClassroomを作成

  3. 作成されたClassroomに必要なアカウントを教師として招待

  4. Log.gsで作成結果をLogシートに出力

  5. HTMLへ返し、作成結果を表示


(1) Config.gs

共通設定をまとめるファイル

Config.gsでは、ほかのファイルから共通して使用する設定を定義しています。

設定シートやLogシートの名称

const CONFIG_SHEET_NAME = '設定';
const LOG_SHEET_NAME = 'Log';

このようにしておくと、シート名を変更する場合は、Config.gsだけを修正すれば済みます。

設定シートの設定データの範囲と行番号

Classroomのセクション名として使用する学校名や、オーナーのアカウント、常時招待するアカウントが入力される設定データの範囲と行番号を定義します。

/**
 * 設定シートの各種設定データの範囲
 */
const CONFIG_DATA_RANGE = 'B2:B';
/**
 * 設定シートのSection名のある行番号
 */
const CONFIG_SECTION_NAME_ROW = 2;
/**
 * CONFIG_DATA_RANGEの開始行番号
 * 例:CONFIG_DATA_RANGEが「B2:B」の場合は2
 */
const CONFIG_DATA_START_ROW = 2;
/**
 * 設定シートのオーナーのアカウントのある行番号
 */
const CONFIG_OWNER_ACCOUNT_ROW = 5;
/**
 * 設定シートの常時招待メールアドレス開始行番号
 */
const CONFIG_ALWAYS_INVITE_ACCOUNT_START_ROW = 6;

行番号を定数として分けておくことで、スプレッドシートのレイアウトを変更した場合にも対応しやすくなります。

スプレッドシートからのデータ取得回数を最小限にするために、
CONFIG_DATA_RANGEで設定データの範囲を定義しています。

CONFIG_DATA_START_ROWは、設定データの開始行番号で、
「2」に設定しています。

CONFIG_SECTION_NAME_ROWは、ClassroomのSection名に入れるための学校名などが入力される行番号です。

CONFIG_OWNER_ACCOUNT_ROWは、オーナーのアカウントが入力される行番号です。
B5セルに入力されるので「5」に設定しています。

メールアドレスを含む設定値は、CONFIG_DATA_RANGEで指定したB2セル以降からまとめて取得します。
Ver.2では、列番号を個別に指定せず、取得範囲と各項目の行番号を組み合わせて設定値を読み取ります。

CONFIG_ALWAYS_INVITE_ACCOUNT_START_ROWは、管理職などの常時招待されるアカウントを読み取り始める行番号です。

Classroomを作成したときの初期状態

作成したClassroomを、作成直後から利用できる状態にするため、初期状態をACTIVEに設定します。

/**
 * Classroomを作成したときの初期状態
 */
const STARTED_COURSE_STATUS = 'ACTIVE';

この値は、Classroomを作成する際のcourseStateに使用します。

Webアプリのタイトル

Webアプリで使用するタイトルも定数として管理します。

/**
 * HTMLタイトル
 */
const HTML_TITLE_NAME = 'Classroom一括作成フォーム';

この定数は、Webアプリを開いたときのブラウザのタブ名として使用します。
Webアプリ画面内の見出しは、index.htmlのh1要素で設定しています。

Classroom数の上限

1回の実行で作成できるClassroom数の上限を定義しています。

/**
 * Classroom一括作成数の上限
 *
 * サーバー負荷とAPI実行時間を抑えるため。
 */
const MAX_CLASSROOM_COUNT = 12;

処理時間の増加やタイムアウトを防ぐために、最大12件で設定しています。


(2) WebApp.gs

Webアプリの初期画面を返す

WebApp.gsには、Webアプリへアクセスしたときに実行されるdoGet関数を記述しています。

/**
 * Webアプリの初期画面を返す。
 *
 * @return {GoogleAppsScript.HTML.HtmlOutput}
 */
function doGet() {
  return HtmlService
    .createHtmlOutputFromFile("index")
    .setTitle(HTML_TITLE_NAME);
}

createHtmlOutputFromFile('index')で、index.htmlを読み込みます。
続くsetTitleでは、Config.gsで定義したタイトルをブラウザのタブへ設定します。
GASをWebアプリとして公開した場合、発行されたURLへアクセスすると、このdoGet関数が実行されます。


(3) Utils.gs

共通処理をまとめる

Utils.gsには、複数のファイルから使用する小さな共通処理をまとめています。

メールアドレス形式を確認する

/**
 * メールアドレス形式を簡易チェックする。
 *
 * @param {string} email
 * @return {boolean}
 */
function isValidEmail_(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

この関数は、入力された文字列がメールアドレスの基本的な形式になっているかを確認します。
文字列に、

  • @が含まれているか

  • @の前後に文字があるか

  • ドメイン部分に.が含まれているか

といった、基本的な形式を確認しています。

改行付きの文字列を配列へ変換する

/**
 * 改行区切りのテキストを配列化する。
 *
 * @param {string} text
 * @return {string[]}
 */
function parseLines_(text) {
  return String(text || '')
    .split(/\r?\n/)
    .map(line => line.trim())
    .filter(Boolean);
}

Webアプリでは、複数のClassroom名やメールアドレスを、1行に1件ずつ入力します。
この関数では、入力された文字列を改行ごとに分割します。
その後、

.map(line => line.trim())

で各行の前後にある空白を取り除きます。
さらに、

.filter(Boolean)

で空の行を除外します。
最終的に、入力されたデータを1件ずつ処理できる配列として返します。

各シートを取得する

設定シートとLogシートを取得します。

/**
 * 設定シートを取得する。
 */
function getConfigSheet_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const shConfig = ss.getSheetByName(CONFIG_SHEET_NAME);
  // シートの存在確認
  if (!shConfig) {
    throw new Error(`${CONFIG_SHEET_NAME}シートが存在しません。`);
  }
  return shConfig;
}
/**
 * Logシートを取得する。
 */
function getLogSheet_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let shLog = ss.getSheetByName(LOG_SHEET_NAME);
  // シートがない場合は新規作成して見出しを追加する
  if (!shLog) {
    shLog = ss.insertSheet(LOG_SHEET_NAME);
    shLog.appendRow([
      '日時',
      'Classroom名',
      'ClassroomID',
      'オーナー',
      '実行者',
      '作成結果',
      '作成エラー',
    ]);
  }
  return shLog;
}

エラーからメッセージを取得する

エラーメッセージを文字列として取得します。

/**
 * エラーからメッセージを取得する
 *
 * @param {*} error
 * @return {string}
 */
function getErrorMessage_(error) {
  if (
    error &&
    typeof error.message === 'string'
  ) {
    return error.message;
  }

  return String(error);
}

messageプロパティがある場合はその内容を返し、
それ以外の場合は文字列へ変換して返します。


(4) Account.gs

このファイルでは、設定シートに入力された学校名やオーナーのメールアドレス、常時招待するアカウントなどを取得します。
また、Webアプリを実行したユーザーや、画面上で追加された教師のメールアドレスをまとめ、実際に招待するアカウントの一覧を作成します。

各変数について

  • sectionName

    • Classroomのセクション名

  • ownerEmail

    • Classroomのオーナー

  • requesterEmail

    • Webアプリを実行しているユーザー

  • alwaysInviteEmails

    • 管理職や担当者などの常時招待するアカウント

設定情報をまとめて取得する

Webアプリの起動時に必要な設定情報をまとめて取得します。

/**
 * 現在の設定情報をスプレッドシートから取得する
 * 
 * @return {Object}
 */
function getConfigSettings() {
  // スプレッドシートの取得
  const shConfig = getConfigSheet_();
  // 設定情報を読み取る
  const configValues = shConfig.getRange(CONFIG_DATA_RANGE).getValues();

  // Section名
  const sectionName = String(getConfigValueByRow_(configValues, CONFIG_SECTION_NAME_ROW) || "").trim();

  // オーナーのアカウント
  const ownerEmail = normalizeEmail_(getConfigValueByRow_(configValues, CONFIG_OWNER_ACCOUNT_ROW));
  // 空欄の場合
  if (!ownerEmail) {
    throw new Error(
      `B${CONFIG_OWNER_ACCOUNT_ROW}にClassroomオーナーのメールアドレスが入力されていません。`
    );
  }
  // メールアドレス形式を確認
  if (!isValidEmail_(ownerEmail)) {
    throw new Error(
      `B${CONFIG_OWNER_ACCOUNT_ROW}のメールアドレス形式が正しくありません: ${ownerEmail}`
    );
  }

  // 常時招待する管理職・教師のメールアドレス一覧
  const alwaysInviteEmails = getAlwaysInviteEmails_(configValues);

  // 実行者のメールアドレス
  const requesterEmail = normalizeEmail_(Session.getActiveUser().getEmail());

  return {
    sectionName,
    ownerEmail,
    requesterEmail,
    alwaysInviteEmails,
  };
}

処理の流れは次のとおりです。

  1. 設定シートから設定データを範囲で取得する

  2. 取得したデータから
    セクション名
    オーナーのアカウント
    常時招待するアカウント
    を取得する

  3. Webアプリを実行したユーザーを取得する

  4. 取得した情報をオブジェクトにまとめて返す

この関数の戻り値は、Webアプリの初期画面を表示するときに使用します。画面を開いた時点で、

  • Classroomのオーナー

  • 常時招待される管理職や担当者

  • 現在ログインしている実行者

などを表示できるのは、この関数で情報をまとめているためです。

設定データから指定行の値を取得する

次の関数では、設定シートから指定した行番号のデータを読み取ります。

/**
 * 設定データから指定行の値を取得する
 *
 * @param {Object[][]} configValues 設定データ
 * @param {number} sheetRow シート上の行番号
 * @return {*} セルの値
 */
function getConfigValueByRow_(configValues, sheetRow) {
  // configValues内での行番号
  const rowIndex = sheetRow - CONFIG_DATA_START_ROW;

  // 範囲外の場合は空文字を返す
  if (rowIndex < 0 || rowIndex >= configValues.length) {
    return '';
  }

  return configValues[rowIndex][0];
}

メールアドレスを正規化する

この関数では、受け取ったメールアドレスを文字列に変換し、
前後の空白を取り除いたうえで、英字を小文字に統一します。
メールアドレスの表記を統一することで、
大文字と小文字の違いや余分な空白による重複、比較ミスを防ぎます。

/**
 * メールアドレスを正規化する
 *
 * @param {*} email
 * @return {string}
 */
function normalizeEmail_(email) {
  return String(email || '').trim().toLowerCase();
}

常時招待するアカウントを取得する

次の関数では、設定シートに入力された管理職や担当者などのメールアドレスをまとめて取得します。

/**
 * 常時招待メールアドレス一覧を取得
 *
 * @param {Object[][]} configValues 設定シートから取得した設定データ
 * @return {string[]} 常時招待メールアドレス一覧
 */
function getAlwaysInviteEmails_(configValues) {
  // configValues内での常時招待アカウント開始位置
  const startIndex =
    CONFIG_ALWAYS_INVITE_ACCOUNT_START_ROW - CONFIG_DATA_START_ROW;

  // 常時招待メールアドレス一覧を整形
  const emails = configValues
    .slice(startIndex)
    .map(row => row[0])
    .map(normalizeEmail_)
    .filter(Boolean);

  // 不正なメールアドレス一覧
  const invalidEmails = emails.filter(
    email => !isValidEmail_(email)
  );

  if (invalidEmails.length > 0) {
    throw new Error(
      '設定シートに形式が正しくないメールアドレスがあります。\n\n' +
      invalidEmails.join('\n')
    );
  }

  // 重複を除去して返す
  return [...new Set(emails)];
}

まず、常時招待アカウントの開始行番号を、取得済みの配列内の位置へ変換します。
次に、slice()を使って、その位置から配列の末尾までを切り出します。

Ver.2では、設定値ごとにスプレッドシートへアクセスするのではなく、B2セル以降を一度に取得し、その配列から必要な範囲を取り出しています。これにより、シートへのアクセス回数を減らしています。

スプレッドシートから複数のセルをまとめて取得すると、データは二次元配列として返されます。そのため、必要なメールアドレスだけを取り出し、

  • 前後の空白を除く

  • 空欄を除く

  • メールアドレスの形式を確認する

という処理を行います。
最終的には、次のような一次元配列として返します。

[
  "admin1@example.jp",
  "admin2@example.jp",
  "teacher@example.jp"
]

管理職の人数が増減しても、設定シートへ行を追加するだけで対応できる構成です。

招待するメールアドレス一覧を作成する

最後の関数では、実際にClassroomへ招待するメールアドレスを1つの配列にまとめます。

/**
 * 招待対象メールアドレス一覧を作成
 *
 * @param {string} ownerEmail
 * @param {string} requesterEmail
 * @param {string[]} alwaysInviteEmails
 * @param {string[]} extraTeacherEmails
 * @return {string[]}
 */
function buildInviteEmailList_(
  ownerEmail,
  requesterEmail,
  alwaysInviteEmails,
  extraTeacherEmails
) {
  // オーナーのアカウント
  const normalizedOwnerEmail = normalizeEmail_(ownerEmail);
  // メールアドレス一覧
  const emails = [
    requesterEmail,
    ...alwaysInviteEmails,
    ...extraTeacherEmails
  ]
    .map(normalizeEmail_)
    .filter(Boolean)
    .filter(email => email !== normalizedOwnerEmail);

  // 不正なメールアドレス一覧
  const invalidEmails = emails.filter(
    email => !isValidEmail_(email)
  );

  // 不正なメールアドレスがある場合は処理を中止
  if (invalidEmails.length > 0) {
    throw new Error(
      '招待対象に形式が正しくないメールアドレスがあります。\n\n' +
      invalidEmails.join('\n')
    );
  }

  return [...new Set(emails)];
}

対象となるのは次のアカウントです。

  • 設定シートに登録された常時招待アカウント

  • Webアプリを実行したユーザー

  • 画面上で追加された教師アカウント

これらを1つの一覧へまとめたあと、次の処理を行います。

  • 前後の空白を取り除く

  • 空欄を除く

  • 重複するメールアドレスを除く

  • メールアドレスの形式を確認する

  • オーナーと同じアカウントを招待対象から除く

オーナーはすでにClassroomの所有者として登録されるため、教師として重ねて招待する必要はありません。
常時招待アカウント、実行者、追加教師をまとめたうえで、最終的な招待一覧を作る構成になっています。


(5) Classroom.gs

このツールの中心となるファイルです。
HTML側から受け取った情報を使ってClassroomを作成し、管理職や教師を招待します。
主な処理は次のとおりです。

  • HTML側から入力内容を受け取る

  • Classroom名を整理する

  • 設定情報を取得する

  • 追加教師のメールアドレスを確認する

  • Classroomを1件ずつ作成する

  • 教師を招待する

  • 作成結果をまとめる

  • 結果をHTML側へ返す

Classroomの作成処理を統括する

最初の関数は、HTML側から呼び出され、Classroomの一括作成を管理する関数です。

/**
 * HTML側から呼び出されるメイン処理
 *
 * @param {string} classNamesText Classroom名一覧テキスト
 * @param {string} extraTeachersText 追加教師メールアドレス一覧テキスト
 * @return {Object} 作成結果
 */
function createClassroomsFromHtml(classNamesText, extraTeachersText) {
  // クラス名
  const classNames = parseLines_(classNamesText);
  if (classNames.length === 0) {
    throw new Error('Classroom名が入力されていません。');
  }

  // 一括作成Classroom数の上限
  if (classNames.length > MAX_CLASSROOM_COUNT) {
    throw new Error(
      `一度に作成できるClassroomは${MAX_CLASSROOM_COUNT}件までです。\n` +
      `現在の入力件数: ${classNames.length}件`
    );
  }

  // 設定情報の取得
  const {
    sectionName,
    ownerEmail,
    alwaysInviteEmails,
    requesterEmail,
  } = getConfigSettings();

  // アクセス者のメールアドレスが取得できない場合
  if (!requesterEmail) {
    throw new Error(
      'アクセス者のメールアドレスを取得できませんでした。\n' +
      'Webアプリの公開設定を「組織内のユーザー」にしているか確認してください。'
    );
  }

  // 追加教師アカウント一覧
  const extraTeacherCandidates =
    parseLines_(extraTeachersText)
      .map(normalizeEmail_);
  const invalidExtraEmails =
    extraTeacherCandidates.filter(
      email => !isValidEmail_(email)
    );

  // 追加教師メールアドレス欄に形式が正しくないものがある場合
  if (invalidExtraEmails.length > 0) {
    throw new Error(
      '追加教師メールアドレス欄に形式が正しくないものがあります。\n\n' +
      invalidExtraEmails.join('\n')
    );
  }
  const extraTeacherEmails = [...new Set(extraTeacherCandidates)];

  // 結果の格納場所
  const results = [];

  // クラスルーム作成
  classNames.forEach(className => {
    const result = createOneClassroom_(
      className,
      sectionName,
      ownerEmail,
      requesterEmail,
      alwaysInviteEmails,
      extraTeacherEmails
    );

    results.push(result);
  });

  // HTMLへ渡すデータ
  const responseResults = results.map(result => ({
    className: result.className,
    courseId: result.courseId,
    ownerEmail: result.ownerEmail,
    requesterEmail: result.requesterEmail,
    createStatus: result.createStatus,
    createMessage: result.createMessage,
    inviteResults: result.inviteResults,
  }));

  return {
    requesterEmail,
    ownerEmail,
    alwaysInviteEmails,
    extraTeacherEmails,
    results: responseResults,
  };
}

Webアプリの入力欄から、次のデータを受け取ります。

  • 作成するClassroom名の一覧

  • 追加で招待する教師の一覧

Classroom名は改行区切りで入力されるため、Utils.gsの関数を使って配列へ変換します。
次に、Account.gsから次の設定情報を取得します。

  • セクション名

  • オーナーのアカウント

  • 常時招待するアカウント

  • Webアプリの実行者

追加教師についても、空欄や前後の空白を処理したあと、メールアドレスの形式を確認します。入力されたメールアドレスに誤りがある場合は、Classroomを作成する前に処理を中断します。
これにより、一部のClassroomを作成したあとで入力ミスが見つかることを防ぎます。
入力内容に問題がなければ、Classroom名を1件ずつ、1つのClassroomを作成する関数へ渡します。作成結果は配列へ順番に追加し、すべての処理が終了したあと、HTML側へまとめて返します。

1件のClassroomを作成する

次は、1件のClassroomを作成する中心となる関数です。
最初に、処理結果を記録するための変数を用意します。

/**
 * Classroomを1件作成し、教師を招待する
 *
 * @param {string} className
 * @param {string} sectionName
 * @param {string} ownerEmail
 * @param {string} requesterEmail
 * @param {string[]} alwaysInviteEmails
 * @param {string[]} extraTeacherEmails
 * @return {Object}
 */
function createOneClassroom_(
  className,
  sectionName,
  ownerEmail,
  requesterEmail,
  alwaysInviteEmails,
  extraTeacherEmails
) {
  // 日付
  const date = new Date();

  // コースID
  let courseId = '';
  // 状態
  let createStatus = '';
  // 詳細
  let createMessage = '';

  // 招待結果を格納
  const inviteResults = [];

  // Classroomを作成し、教師を招待
  try {
    // コース
    const course = {
      name: className,
      section: sectionName,
      ownerId: ownerEmail,
      courseState: STARTED_COURSE_STATUS
    };

    // コース作成
    const createdCourse = Classroom.Courses.create(course);
    courseId = createdCourse.id;
    createStatus = '作成成功';

    // 招待アカウント一覧を作成
    const inviteEmails = buildInviteEmailList_(
      ownerEmail,
      requesterEmail,
      alwaysInviteEmails,
      extraTeacherEmails
    );

    // 招待対象を教師として招待
    inviteEmails.forEach(email => {
      try {
        // 教師を招待
        inviteTeacher_(courseId, email);

        // 招待成功の結果を格納
        inviteResults.push({
          email,
          status: '招待成功',
          message: ''
        });
      } catch (error) {
        const errorMessage = getErrorMessage_(error);

        inviteResults.push({
          email,
          status: '招待失敗',
          message: errorMessage,
        });

        console.error(`招待失敗: ${email}`, error);
      }
    });
  } catch (error) {
    createStatus = '作成失敗';
    createMessage = getErrorMessage_(error);
  }

  // ログを出力
  try {
    writeLog_({
      date,
      className,
      courseId,
      ownerEmail,
      requesterEmail,
      createStatus,
      createMessage,
    });
  } catch (error) {
    console.error(
      `ログ出力失敗: ${className}`,
      error
    );
  }

  // HTML表示用
  return {
    className,
    courseId,
    ownerEmail,
    requesterEmail,
    createStatus,
    createMessage,
    inviteResults,
  };
}

主な項目は次のとおりです。

  • 作成日時

  • Classroom名

  • ClassroomのID

  • オーナー

  • 実行者

  • 作成状態

  • エラーメッセージ

続いて、Classroom APIへ渡すcourseオブジェクトを作成します。
主なデータ構造は次のとおりです。

  • name

    • Classroom名

  • section

    • セクション名

  • ownerId

    • オーナーのメールアドレス

  • courseState

    • Classroomの状態

courseStateには、Config.gsで定義したACTIVEを設定します。
これにより、作成直後から利用できる状態でClassroomを作成します。
courseオブジェクトを使ってClassroomを作成すると、作成されたコースの情報が返されます。その中からClassroomのIDを取得し、教師を招待する処理やログ出力に使用します。


作成したClassroomへ教師を招待する

Classroomを作成したあと、Account.gsの関数を使って、招待するメールアドレス一覧を作成します。一覧に含まれるアカウントを、1件ずつ教師として招待します。招待対象は、主に次のアカウントです。

  • 管理職

  • 学校の担当者

  • Webアプリを実行したユーザー

  • 追加で指定された教師

招待処理には、作成したClassroomのIDと、招待するユーザーのメールアドレスを渡します。

作成中にエラーが発生した場合

Classroomの作成処理は、エラーが発生する可能性があります。
たとえば、次のような場合です。

  • オーナーに指定したアカウントに必要な権限がない

  • Classroom APIが有効になっていない

  • Classroomの作成上限に達している

  • Google Workspaceの管理設定で操作が制限されている

エラーが発生した場合は、処理結果へ作成状態とエラーメッセージを記録します。これにより、どのClassroomの作成に成功し、どのClassroomでエラーが発生したかをあとから確認できます。

作成結果をまとめる

Classroomの作成処理が終了したら、作成状態やエラーメッセージなどをまとめます。
ログ出力時には、日時、Classroom名、ID、オーナー、実行者、作成状態、エラーメッセージをwriteLog_()へ渡します。

  • date

    • 作成日時

  • className

    • Classroom名

  • courseId

    • Classroom ID

  • ownerEmail

    • オーナー

  • requesterEmail

    • Webアプリの実行者

  • createStatus

    • 作成状態

  • createMessage

    • エラーメッセージ

HTML側へ返すデータには、これらに加えて教師招待結果も含めます。
ログ記録用と画面表示用で必要な項目を分けることで、それぞれの用途に適したデータを返しています。

教師として招待する

次の関数では、ClassroomのIDとメールアドレスを指定し、ユーザーを教師として招待します。

/**
 * 教師としてClassroomへ招待する
 *
 * @param {string} courseId ClassroomのID
 * @param {string} email 招待するメールアドレス
 */
function inviteTeacher_(courseId, email) {
  const invitation = {
    courseId: courseId,
    userId: email,
    role: 'TEACHER'
  };

  return Classroom.Invitations.create(invitation);
}

招待されたユーザーには、Google Classroomから招待が通知されます。
このツールでは、1人の教師の招待に失敗しても、ほかのアカウントの招待処理を継続します。たとえば、1件のメールアドレスに問題があっても、残りの管理職や教師の招待まで止まらないようにしています。


(6) Log.gs

このファイルでは、Classroomの作成結果をLogシートへ記録します。

/**
 * Classroomの作成履歴をLogシートに記録する。
 *
 * @param {Object} result 作成結果
 * @param {Date} result.date 実行日時
 * @param {string} result.className Classroom名
 * @param {string} result.courseId Classroom ID
 * @param {string} result.ownerEmail オーナー
 * @param {string} result.requesterEmail 実行者
 * @param {string} result.createStatus 作成結果
 * @param {string} result.createMessage エラーメッセージ
 */
function writeLog_(result) {
  // スプレッドシートの取得
  const shLog = getLogSheet_();
  // ログの出力
  shLog.appendRow([
    result.date,
    result.className,
    result.courseId,
    result.ownerEmail,
    result.requesterEmail,
    result.createStatus,
    result.createMessage,
  ]);
}

Classroomを一括作成する場合、画面に結果を表示するだけでは、あとから確認できなくなる可能性があります。
そこで、次のような情報をスプレッドシートへ残します。

  • 実行日時

  • Classroom名

  • ClassroomのID

  • オーナー

  • 実行者

  • 作成結果

  • エラーメッセージ

Logシートを準備する

最初に、ログを記録するシートが存在するかを確認します。
Logシートが存在しない場合は、新しいシートを作成します。
さらに、最初の行へ見出しを追加します。
一度シートを作成したあとは、同じシートを継続して利用します。
そのため、利用者があらかじめLogシートを用意していなくても、初回実行時に自動で作成されます。

作成結果を追記する

Classroom.gsで作成したresultオブジェクトから、ログに必要な情報を取り出します。取り出した情報は1行の配列へ整形し、Logシートの最終行へ追加します。複数のClassroomを作成した場合も、1件につき1行ずつ記録されます。
これにより、

  • いつ作成したか

  • 誰が実行したか

  • どのオーナーで作成したか

  • ClassroomのIDは何か

  • 作成に成功したか

  • どのようなエラーがあったか

をあとから確認できます。
学校で継続的に利用する場合、処理履歴を残せることは、引き継ぎやトラブル対応の面でも重要です。


(7) index.html

このファイルには、Webアプリの画面、デザイン、ブラウザ側で動作するJavaScriptを記述しています。
大きく分けると、次の3つで構成されています。

  • HTML:画面の構造

  • CSS:画面のデザイン

  • JavaScript:ボタン操作やGASとの通信

CSSで画面のデザインを設定する

head内には、Webアプリのデザインを設定するCSSを記述します。
CSSは記述量が多いため、本記事では役割のみを説明します。全文は、配布しているソースコードをご確認ください。

主に次の項目を指定しています。

  • 画面全体の幅

  • 入力欄の大きさ

  • 余白

  • 背景色

  • ボタンのデザイン

  • 結果表示欄の配置

  • エラーメッセージの表示

機能だけでなく、学校現場で迷わず操作できるように、入力欄と実行ボタンをシンプルに配置しています。

bodyで画面の構造を作る

body内には、実際に画面へ表示する要素を記述します。

  <body>
    <main class="container">
      <h1>Classroom一括作成フォーム</h1>

      <!-- 実行者表示欄 -->
      <div id="loginInfo">
        ログインユーザー確認中...
      </div>

      <!-- 設定情報表示欄 -->
      <div id="settingInfo">
        設定情報確認中...
      </div>

      <!-- Classroom名一覧 -->
      <label for="classNames">
        作成するClassroom名
      </label>

      <textarea
        id="classNames"
        rows="6"
        placeholder="例:
令和8年度 1年1組
令和8年度 1年2組
令和8年度 1年3組
令和8年度 国語科
令和8年度 サッカー部"
      ></textarea>

      <div class="note">
        複数作成する場合は、1行に1つずつClassroom名を入力してください。
      </div>

      <!-- 追加招待教師一覧 -->
      <label for="extraTeachers">
        追加で招待したい教師メールアドレス
      </label>

      <textarea
        id="extraTeachers"
        rows="3"
        placeholder="例:
teacher1@example.co.jp
teacher2@example.co.jp"
      ></textarea>

      <div class="note">
        追加教師がいる場合のみ入力してください。<br>
        フォームを開いたユーザーと、設定シートB6以降の管理職・教師メールアドレスは自動で教師招待されます。
      </div>

      <!-- 作成ボタン -->
      <button
        id="createButton"
        type="button"
        onclick="createClassrooms()"
        disabled
      >
        Classroomを作成
      </button>

      <!-- ステータス表示欄 -->
      <div id="status">
        待機中
      </div>

      <!-- 作成結果一覧 -->
      <div
        id="resultList"
        class="result-list"
      ></div>

      <!-- 共通操作ボタン -->
      <div id="commonActions">
        <strong>作成後の確認</strong>

        <div class="action-area">
          <a
            class="action-link classroom-link"
            href="https://classroom.google.com/"
            target="_blank"
            rel="noopener noreferrer"
          >
            Classroomを開いて確認
          </a>

          <a
            class="action-link gmail-link"
            href="https://mail.google.com/mail/"
            target="_blank"
            rel="noopener noreferrer"
          >
            Gmailで通知を確認
          </a>
        </div>
      </div>
    </main>

主な表示内容は次のとおりです。

  • Webアプリのタイトル

  • ログインしている実行者

  • Classroomのオーナー

  • 常時招待するアカウント

  • Classroom名の入力欄

  • 追加教師の入力欄

  • Classroomを作成するボタン

  • 処理状況や結果の表示欄

Classroom名と追加教師の入力欄には、placeholderを設定しています。placeholderは、入力欄が空のときに表示する入力例です。
たとえば、Classroom名の入力欄に、

令和8年度 1年1組
令和8年度 1年2組
令和8年度 1年3組
令和8年度 国語科
令和8年度 サッカー部

のような例を表示しておけば、1行につき1件入力することが伝わりやすくなります。

画面読み込み時の処理

1つ目のJavaScript関数は、Webアプリの画面が読み込まれたときに実行されます。

    <script>
      /**
       * 起動時に設定情報を表示する。
       */
      window.onload = function() {
        google.script.run
          .withSuccessHandler(function(settings) {
            // ログインユーザーの表示欄
            const loginInfo =
              document.getElementById('loginInfo');

            // ログインユーザーのメールアドレスを表示
            if (settings.requesterEmail) {
              loginInfo.textContent =
                'ログインユーザー: ' +
                settings.requesterEmail;
            } else {
              loginInfo.textContent =
                'ログインユーザーのメールアドレスを取得できませんでした。';
            }

            // 設定情報の表示欄
            const settingInfo =
              document.getElementById('settingInfo');
            // 設定情報の表示欄の内容を作成
            let text = '';

            text +=
              'Classroomオーナー: ' +
              settings.ownerEmail +
              '\n';

            text +=
              '常時招待する管理職・教師: ';

            if (
              !settings.alwaysInviteEmails ||
              settings.alwaysInviteEmails.length === 0
            ) {
              text += 'なし';
            } else {
              text +=
                '\n' +
                settings.alwaysInviteEmails
                  .map(function(email) {
                    return '・' + email;
                  })
                  .join('\n');
            }

            settingInfo.textContent = text;

            // Classroom作成ボタンを有効化
            document
              .getElementById('createButton')
              .disabled = false;
          })
          .withFailureHandler(function(error) {
            // ログインユーザーの表示欄
            document
              .getElementById('loginInfo')
              .textContent =
                '初期設定取得エラー';

            // 設定情報の表示欄
            document
              .getElementById('settingInfo')
              .textContent =
                getErrorMessage(error);

            // Classroom作成ボタンを無効化
            document
              .getElementById('createButton')
              .disabled = true;
          })
          .getConfigSettings();
      };

google.script.runを使って、GAS側のgetConfigSettings関数を呼び出します。取得した設定情報を使って、画面上に次の内容を表示します。

  • ログインしている実行者

  • Classroomのオーナー

  • 常時招待するアカウント

GAS側の処理が成功した場合に実行する関数と、失敗した場合に実行する関数を、それぞれ指定しています。設定情報を取得できなかった場合は、画面上にエラーメッセージを表示します。

ボタンを押したときの処理

2つ目のJavaScript関数は、「Classroomを作成」ボタンが押されたときに実行されます。

      /**
       * Classroomを一括作成する。
       */
      function createClassrooms() {
        // Classroom名一覧を取得
        const classNamesText =
          document
            .getElementById('classNames')
            .value
            .trim();

        const classNames = parseLines(classNamesText);

        if (classNames.length === 0) {
          alert(
            'Classroom名を入力してください。'
          );
          return;
        }

        // 追加招待教師一覧を取得
        const extraTeachersText =
          document
            .getElementById('extraTeachers')
            .value
            .trim();

        // ステータス表示欄を取得
        const status =
          document.getElementById('status');

        // Classroom作成ボタンを取得
        const button =
          document.getElementById('createButton');

        // 結果表示欄を取得
        const resultList =
          document.getElementById('resultList');

        // 共通操作欄を取得
        const commonActions =
          document.getElementById('commonActions');

        // 前回の結果を消去
        resultList.replaceChildren();
        commonActions.style.display = 'none';

        // 二重実行を防止
        button.disabled = true;

        status.textContent =
          'Classroomを作成しています...';

        google.script.run
          .withSuccessHandler(function(result) {
            // Classroom作成ボタンを有効化
            button.disabled = false;

            // Classroom作成結果を表示
            showCreationResults(result);

            // ステータス表示欄に完了メッセージを表示
            status.textContent =
              'Classroom作成処理が完了しました。';
          })
          .withFailureHandler(function(error) {
            // Classroom作成ボタンを有効化
            button.disabled = false;

            // ステータス表示欄にエラーメッセージを表示
            status.textContent =
              'エラーが発生しました。\n\n' +
              getErrorMessage(error);
          })
          .createClassroomsFromHtml(
            classNamesText,
            extraTeachersText
          );
      }

まず、入力欄から次の情報を取得します。

  • Classroom名の一覧

  • 追加教師のメールアドレス一覧

次に、処理状況を表示する欄と、実行ボタンの情報を取得します。
処理中にボタンを何度も押されると、同じClassroomが重複して作成される可能性があります。そのため、処理開始時にボタンを押せない状態へ変更します。
続いて、google.script.runを使って、GAS側のcreateClassroomsFromHtml関数を呼び出します。
処理が成功した場合は、作成完了のメッセージと実行結果を画面へ表示します。処理に失敗した場合は、エラー内容を表示します。
どちらの場合も、処理終了後にボタンを再び押せる状態へ戻します。

Classroomの作成結果を表示する

3つ目のJavaScript関数では、Classroomの作成結果をカード形式にして表示します。

      /**
       * Classroom作成結果を表示する。
       *
       * @param {Object} result サーバーから返された作成結果
       */
      function showCreationResults(result) {
        // 結果表示欄を取得
        const resultList =
          document.getElementById('resultList');

        // 共通操作欄を取得
        const commonActions =
          document.getElementById('commonActions');

        // 結果を配列化
        const results =
          result && Array.isArray(result.results)
            ? result.results
            : [];

        // 作成成功したClassroomが1件以上あるかどうかのフラグ
        let hasSuccessfulClassroom = false;

        // 作成結果をカード形式で表示
        results.forEach(function(item) {
          const isSuccess =
            item.createStatus === '作成成功' && Boolean(item.courseId);

          if (isSuccess) {
            hasSuccessfulClassroom = true;
          }

          // 結果カードを作成
          const card =
            document.createElement('section');

          card.className =
            'result-card ' +
            (isSuccess ? 'success' : 'failure');

          // Classroom名を表示
          const title =
            document.createElement('h2');

          title.className = 'result-title';
          title.textContent =
            item.className || 'Classroom名なし';

          card.appendChild(title);

          // 作成結果を表示
          const statusText =
            document.createElement('p');

          statusText.className = 'result-detail';
          statusText.textContent =
            '作成結果: ' +
            (item.createStatus || '不明');

          card.appendChild(statusText);

          // Classroom IDを表示(作成成功時のみ)
          if (item.courseId) {
            const courseIdText =
              document.createElement('p');

            courseIdText.className =
              'result-detail';

            courseIdText.textContent =
              'Classroom ID: ' +
              item.courseId;

            card.appendChild(courseIdText);
          }

          // 作成時の詳細メッセージを表示
          if (item.createMessage) {
            const message =
              document.createElement('p');

            message.className =
              'result-detail warning';

            message.textContent =
              '詳細: ' +
              item.createMessage;

            card.appendChild(message);
          }

          // 教師招待結果を表示
          appendInviteResults(
            card,
            item.inviteResults
          );

          // 結果カードを結果表示欄に追加
          resultList.appendChild(card);
        });

        // 1件以上作成成功した場合だけ共通操作ボタンを表示
        commonActions.style.display =
          hasSuccessfulClassroom
            ? 'block'
            : 'none';
      }

教師招待結果を結果カードに追加する

4つ目のJavaScript関数では、教師招待結果をClassroomの作成結果カードへ追加します。
招待結果が空の場合は何も追加せず、結果がある場合は、教師ごとにメールアドレス、招待状態、エラーメッセージを表示します。

      /**
       * 教師招待結果を結果カードに追加する。
       *
       * @param {HTMLElement} card 結果カード
       * @param {Object[]} inviteResults 招待結果
       */
      function appendInviteResults(
        card,
        inviteResults
      ) {
        // 招待結果が空の場合は何もしない
        if (
          !Array.isArray(inviteResults) ||
          inviteResults.length === 0
        ) {
          return;
        }

        // 招待結果を表示するコンテナを作成
        const container =
          document.createElement('div');

        container.className =
          'invite-results';

        // 見出しを作成
        const heading =
          document.createElement('strong');

        heading.textContent =
          '教師招待結果';

        container.appendChild(heading);

        // 招待結果を1件ずつ表示
        inviteResults.forEach(
          function(inviteResult) {
            // 招待結果の1行を作成
            const line =
              document.createElement('div');

            // 招待成功かどうか
            const isSuccess =
              inviteResult.status === '招待成功';

            line.className =
              isSuccess
                ? 'invite-success'
                : 'invite-failure';

            // 招待結果の内容を表示
            line.textContent =
              '・' +
              inviteResult.email +
              ': ' +
              inviteResult.status +
              (
                inviteResult.message
                  ? '(' +
                    inviteResult.message +
                    ')'
                  : ''
              );

            container.appendChild(line);
          }
        );

        // 結果カードに招待結果コンテナを追加
        card.appendChild(container);
      }

改行付きの文字列を配列へ変換する

5つ目のJavaScript関数では、入力欄に記述された改行区切りの文字列を配列へ変換します。

      /**
       * 改行区切りの文字列を配列化する。
       *
       * @param {string} text 対象文字列
       * @return {string[]}
       */
      function parseLines(text) {
        return String(text || '')
          .split(/\r?\n/)
          .map(function(line) {
            return line.trim();
          })
          .filter(function(line) {
            return Boolean(line);
          });
      }

処理内容はUtils.gsの関数と似ています。
ただし、Utils.gsはGAS側で実行されるコードです。
一方、index.html内のJavaScriptはブラウザ側で実行されます。
実行される場所が異なるため、同じような処理をHTML側にも用意しています。この関数では、

  1. 改行ごとに文字列を分割する

  2. 各行の前後の空白を取り除く

  3. 空の行を除く

  4. 配列として返す

という処理を行います。

エラーメッセージを取得する

6つ目のJavaScript関数では、エラーメッセージを取得します。

      /**
       * エラーメッセージを取得する。
       *
       * @param {*} error エラー
       * @return {string}
       */
      function getErrorMessage(error) {
        if (
          error &&
          typeof error.message === 'string'
        ) {
          return error.message;
        }

        return String(error);
      }
    </script>
  </body>
</html>

ソースコード一覧

(1) Config.gs

https://drive.google.com/file/d/17FLujOODcEZD7tkFpJlt5pUCHgC4Z1Ah/view?usp=drive_link

(2) WebApp.gs

https://drive.google.com/file/d/1TyTXo8pIXLPfnEp_br9yGKFxyoFMuH1k/view?usp=drive_link

(3) Utils.gs

https://drive.google.com/file/d/1P2L7hHym9AnWkwVkbuLua7IOTKkLUBiX/view?usp=drive_link

(4) Account.gs

https://drive.google.com/file/d/1GllbAxGiwmndqaWW6K4Q_6iOuAoiavQV/view?usp=drive_link

(5) Classroom.gs

https://drive.google.com/file/d/1fC9sKrDJGn8CQ21T3NrDNBoUfy_DtLE0/view?usp=drive_link

(6) Log.gs

https://drive.google.com/file/d/1nps1OyQxmazfpuazPnd_QzjogaKQtEW8/view?usp=drive_link

(7) index.html

https://drive.google.com/file/d/1QPNS-lUSk364OJC0xzIo4KLHL3cAFLVJ/view?usp=drive_link


まとめ

今回は、Google Classroomを一括作成し、管理職や教師を自動で招待するWebアプリのコード構成を解説しました。
このツールでは、次の処理を連携させています。

  • スプレッドシートから設定情報を取得する

  • WebアプリでClassroom名を入力する

  • HTML側からGAS側の関数を呼び出す

  • Classroomを1件ずつ作成する

  • 管理職や教師を招待する

  • 実行結果をログへ記録する

  • 処理結果をWebアプリへ表示する

ファイルごとに役割を分けることで、それぞれの処理の関係が見えやすくなり、修正や機能追加にも対応しやすくしています。



いいなと思ったら応援しよう!

EponaLab よろしければ応援お願いします!