GASでスプレッドシート上の生徒をGoogle Classroomのクラスに招待する(応用編Part2)

GoogleAppsScriptを用いてスプレッドシート上の名簿の生徒をGoogle Classroomのクラスに招待GASの応用編Part2を紹介します。

概要

前回・前々回で作成した「クラスへの副担任・生徒の招待のGAS」を更に改良していきたいと思います。

前々回の記事
⇒Google Classroomのクラスへ、副担任・生徒を招待するGASの紹介。
前回の記事
⇒上記GASをスプレッドシート上で実行できるように応用したものの紹介。

前回までの内容ではクラスに既に参加している副担任や生徒を考慮せず、スプレッドシート上の招待リストをそのまま用いて招待を作成する作りになっていました。
しかし、仕様として招待の作成は重複して行えないため、既に参加している(または招待されている)相手に対して更に招待を作成するとエラーが発生してしまいます。

今回はクラスに既に参加している副担任や生徒とスプレッドシート上の招待リストの比較を行い、未招待のメンバーに対してのみ招待を作成するように改良したいと思います。
実装の流れは以下になります。

  1. クラスに参加している副担任の取得
  2. クラスに参加している生徒の取得
  3. クラスに参加しているメンバーデータの整理
  4. 招待済みメンバーの取得
  5. スプレッドシート上の招待リストとの比較判定の実装
  6. ドメイン外ユーザーを招待した際のエラー処理

1. クラスに参加している副担任の取得

既にクラスに参加している副担任の取得にはClassroom.Courses.Teachers.list(courseId)を使用します。
引数には対象のクラスのクラスIDを必要とします。

既存メンバーを取得する関数getExistMembers(courseId)を作成します。
引数として対象のクラスのIDcourseIdを指定します。

この関数は引数を必要とするため、GASエディターの「実行」や「デバッグ」を使って単体で動かすとエラーが発生します。
動作確認をする際には後述の例のように、別の関数内で引数を与えて呼び出す必要があります。

function getExistMembers(courseId) {
  // 対象クラスに既に参加している担任・副担任データの取得
  const responseTeachers = Classroom.Courses.Teachers.list(courseId);
  // responseTeachersはteachersという配列要素を持ったオブジェクトなので、配列要素を取り出してあげる必要があります。
  // 対象クラスに既に参加している担任・副担任データを格納する配列を宣言し、格納
  var teachers = responseTeachers.teachers;

  // 担任・副担任データがある場合、名前とIDを実行ログに表示
  if (teachers.length === 0) {
    Logger.log("No teachers found.");
  } 
  else {
    Logger.log("Teachers :");
    for (teacher in teachers) {
      Logger.log('%s (%s)', teachers[teacher].profile.name.fullName, teachers[teacher].userId);
    }
  }
}

ここで得られるteachersという配列データには参加している副担任だけではなく担任(クラスの作成者)も含まれています。
また、招待は送信されているが参加していない状態の副担任は含まれません。

ちなみに、このteachersはクラスに参加している「教師アカウント」ではなく、あくまでクラス内で担任・副担任の役割に就いているアカウントになります。
つまり、生徒としてクラスに参加している「教師アカウント」は含まれません。

動作確認(クラスに参加している副担任の取得)

動作確認の際には下記のようにcreateInvitation()の一部を流用してみると良いです。

function test() {
  const courses = listCourses();
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const mainSheet = ss.getSheetByName('名簿');
  const targetCourseName = mainSheet.getRange("E3").getValue();
  const targetCourse = selectCourse(courses, targetCourseName);

  if(targetCourse) {
    getExistMembers(targetCourse.id);
  }
}

実行結果:

2. クラスに参加している生徒の取得

既にクラスに参加している副担任の取得にはClassroom.Courses.Students.list(courseId)を使用します。
引数には対象のクラスのクラスIDを必要とします。

先ほどの既存メンバーを取得する関数getExistMembers(courseId)に追記します。

function getExistMembers(courseId) {

  const responseTeachers = Classroom.Courses.Teachers.list(courseId);
  var teachers = responseTeachers.teachers;

  // 対象クラスに既に参加している生徒データを取得
  const responseStudents = Classroom.Courses.Students.list(courseId);
  // 対象クラスに既に参加している生徒データを格納する配列を宣言
  var students = [];

  // 対象クラスに既に参加している生徒がいる場合、配列に生徒データを格納
  if(responseStudents.students) {
    students = responseStudents.students;
  }

  if (teachers.length === 0) {
    Logger.log("No teachers found.");
  } 
  else {
    Logger.log("Teachers :");
    for (teacher in teachers) {
      Logger.log('%s (%s)', teachers[teacher].profile.name.fullName, teachers[teacher].userId);
    }
  }

  // 生徒データがある場合、名前とIDを実行ログに表示
  if (students.length === 0) {
    Logger.log("No students found.");
  } 
  else {
    Logger.log("Students :");
    for (student in students) {
      Logger.log('%s (%s)', students[student].profile.name.fullName, students[student].userId);
    }
  }
}

先ほどの担任・副担任データの取得と違い、if文を用いている理由について説明します。
基本的にクラスは作成された時点で少なくとも担任が存在しているため、担任・副担任データの取得では最低1つはデータが取得できるのに対し、生徒データはゼロつまり取得できない可能性があります。
参加している生徒が0人の場合responseStudentsはデータなしのオブジェクト(GASの実行ログだと{}が表示される)となるため、responseStudents.studentsはundefinedとなります。
これを利用して、生徒データが存在する(=参加している生徒が1人以上)の場合は、生徒データを取得するという作りにしています。

また、招待は送信されているが参加していない状態の生徒はこのデータに含まれていません。

動作確認(クラスに参加している生徒の取得)

動作確認の際には1.クラスに参加している副担任の取得で紹介したtest()関数を実行しましょう。

3. クラスに参加しているメンバーデータの整理

この時点で取得しているteachersstudentsは実はデータの構造が違い、また今回は必要ないデータも含まれています。
これらのデータを整理し、まとめてみたいと思います。

追記部分には解説コメントを添えています。(複雑な部分やテクニックの解説は後述します。)
また、実行ログに表示される動作確認部分も助長なので整理します。

function getExistMembers(courseId) {

  // メンバーデータを格納する空配列
  var members = [];

  const responseTeachers = Classroom.Courses.Teachers.list(courseId);
  var teachers = responseTeachers.teachers;

  // map処理を用いて担任・副担任のデータを整形 ※map処理については後述
  teachers = teachers.map(teacher => { 
    return { 
      userId: teacher.userId,
      name: teacher.profile.name.fullName,
      emailAddress: teacher.profile.emailAddress,
      role: 'TEACHER'
    }
  });
  // members配列に整形した担任・副担任のデータを追加 ※concat処理については後述
  members = members.concat(teachers);


  const responseStudents = Classroom.Courses.Students.list(courseId);
  var students = [];

  if(responseStudents.students) {
    students = responseStudents.students;

    // map処理を用いて生徒のデータを整形
    students = students.map(student => { 
      return { 
        userId: student.userId,
        name: student.profile.name.fullName,
        emailAddress: student.profile.emailAddress,
        role: 'STUDENT'
      }
    });
    // members配列に整形した担任・副担任のデータを追加
    members = members.concat(students);
  }

  // メンバーデータがある場合、名前とIDを実行ログに表示
  if (members.length === 0) {
    Logger.log("No members found.");
  } 
  else {
    Logger.log("Members :");
    for (member in members) {
      Logger.log('%s (%s)', members[member].name, members[member].userId);
    }
  }

  // 返り値 参加しているメンバー
  return members;
}

動作確認(クラスに参加しているメンバーデータの整理)

動作確認の際には1.クラスに参加している副担任の取得で紹介したtest()関数を実行しましょう。

map処理について

配列に対して行える処理のひとつで、GASではなくJavascriptに含まれる機能です。
配列のデータ1つ1つに同じ処理を行い、配列を整形して返します。
今回のようなオブジェクトの配列に対しては、要素の取捨選択や追加を行うこともできます。

map処理の例:
[{name: 'りんご', stock: 2, color: 'red'}, {name: 'みかん', stock: 20, color: 'orange'}, {name: 'バナナ', stock: 12, color: 'yellow'}]
上記の配列に対して、以下の処理を加えて整形したいとします。

  1. color要素削除
  2. typeという要素を追加し、値はFRUITSとする
  3. 他の要素はそのまま
function sampleMap() {

  var fruitsList = [
    {name: 'りんご', stock: 2, color: 'red'},
    {name: 'みかん', stock: 20, color: 'orange'},
    {name: 'バナナ', stock: 12, color: 'yellow'}
  ];

  fruitsList = fruitsList.map(fruits => {
    return {
      name: fruits.name,
      stock: fruits.stock,
      type: 'FRUITS'
    }
  });

  console.log(fruitsList);
}

実行結果:
以下のように配列の内容が整形されました。

concat処理について

配列に対して行える処理のひとつで、GASではなくJavascriptに含まれる機能です。
配列にデータを追加するpush処理とは違い、配列を別の配列と合成することができます。

concat処理の例:
['りんご', 'みかん', 'バナナ']という配列と、['スイカ', 'もも']を合成してみます。

function sampleConcat() {

  var fruitsListA = ['りんご', 'みかん', 'バナナ'];
  var fruitsListB = ['スイカ', 'もも'];

  var result;

  result = fruitsListA.concat(fruitsListB);

  console.log('result =' + result);
  console.log('fruitsListA =' + fruitsListA);
  console.log('fruitsListB =' + fruitsListB);
}

実行結果:
以下のように、fruitsListAfruitsListBを合成することができます。
また、上記では結果をresultという別の変数に格納していますが、fruitsListAに格納すればfruitsListAfruitsListBが足されたという動作になります。

4. 作成済みの招待の取得

ここまでで、クラスに既に参加しているメンバーの取得は完了しました。
今度は招待済み(参加は未承諾)のメンバーの取得をしたいと思います。

招待の取得にはClassroom.Invitations.list(option)を使用します。
引数のoptionは下記3つの内容を持つオブジェクトです。
pageToken:指定ページToken(nullの場合は最初のページ)
pageSize:1ページあたりの件数
courseId:招待対象のクラス

これで得られるデータはメンバーのIDまでで、メンバーの名前やメールアドレスまではわかりません。
メンバー詳細の取得にはClassroom.UserProfiles.get(userId)を使用します。
引数のuserIdはメンバーのIDです。

また、招待した相手のドメインが招待の作成元(つまり自分)と違う場合、メンバーのIDuserIdが取得できないようです。
なのでひとまずfilter処理でドメインが違う場合(=userIdが取得できない)を除外します。
ドメインが違う場合の対応は後述の「6.ドメイン外ユーザーを招待した際のエラー処理」で紹介したいと思います。

これらを使用して招待済みメンバーを取得する関数getInvitedMembers(courseId)を作成します。
引数として対象のクラスのIDcourseIdを指定します。

function getInvitedMembers(courseId) {
  // 招待済みメンバーを格納する配列
  var invitedMembers = [];

  var pageToken = null;

  var optionalArgs = {
    pageToken: pageToken,
    pageSize: 100,
    courseId: courseId
  };

  while (true) {
    let response = Classroom.Invitations.list(optionalArgs);

    // 招待のデータがある場合
    if(response.invitations) {
      // userIdが存在する招待データのみinvitedMembersに合成
      invitedMembers = invitedMembers.concat(response.invitations.filter(i => i.userId));
    }

    pageToken = response.nextPageToken;

    if (!pageToken) {
       break;
    }
    else {
      optionalArgs.pageToken = pageToken;
    }
  }

  if (invitedMembers.length === 0) {
    Logger.log("No invitedMembers found.");
  } 
  else {
    Logger.log("invitedMembers :");
    for (invitedMember in invitedMembers) {

      // userIdを用いて取得したメンバーデータを追加
      invitedMembers[invitedMember].profile = Classroom.UserProfiles.get(invitedMembers[invitedMember].userId);

      Logger.log('%s (%s)', invitedMembers[invitedMember].profile.name.fullName, invitedMembers[invitedMember].userId);
    }

    // map処理を用いてメンバーのデータを整形
    invitedMembers = invitedMembers.map(member => {
      return { 
          userId: member.userId,
          name: member.profile.name.fullName,
          emailAddress: member.profile.emailAddress,
          role: member.role
        }
    });
  }
  return invitedMembers;
}

動作確認(作成済みの招待の取得)

動作確認の際には下記のようにcreateInvitation()の一部を流用してみると良いです。

function test2() {
  const courses = listCourses();
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const mainSheet = ss.getSheetByName('名簿');
  const targetCourseName = mainSheet.getRange("E3").getValue();
  const targetCourse = selectCourse(courses, targetCourseName);

  if(targetCourse) {
    getInvitedMembers(targetCourse.id);
  }
}

実行結果:

5. スプレッドシート上の招待リストとの比較判定の実装

ここまでで「既にクラスに参加済みのメンバー」と「招待済みのメンバー」が取得できました。
今度はこれらをスプレッドシート上の招待リストと照合し、一致するものがあった場合はメッセージを表示し招待作成をスキップするようにしたいと思います。

追記部分に解説コメントを載せています。
また、動作確認の際はメッセージボックスを閉じる必要があるので、スプレッドシート上に前回作成した「招待実行ボタン」から実行してください。

function createInvitation() {

  const courses = listCourses();

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const mainSheet = ss.getSheetByName('名簿');

  const targetCourseName = mainSheet.getRange("E3").getValue();
  const targetCourse = selectCourse(courses, targetCourseName);

  if(targetCourse) {

    // 既存メンバーと招待済みメンバーを取得
    const existMembers =getExistMembers(targetCourse.id);
    const invitedMembers = getInvitedMembers(targetCourse.id);

    const members = getMembers();

    members.forEach(member => {

      if(member.role == '副担任'){
        var role = 'TEACHER';
      }
      else if(member.role == '生徒') {
        var role = 'STUDENT';
      }
      else{
        return;
      }

      // 既存メンバーと一致する場合、メッセージを表示して以降の処理をスキップ
      // ※find処理では一致するものがなかった場合undefindとなる
      if(existMembers.find(m=> m.emailAddress == member.mail)){
        Browser.msgBox('「' + member.mail + '」は既に参加しています。');
        return;
      }

      // 招待済みメンバーと一致する場合、メッセージを表示して以降の処理をスキップ
      if(invitedMembers.find(m=> m.emailAddress == member.mail)){
        Browser.msgBox('「' + member.mail + '」は招待済みです。');
        return;
      }

      let invitationData = {
      'userId': member.mail,
      'courseId': targetCourse.id,
      'role': role
      };
    
      Classroom.Invitations.create(invitationData);
    });
    Browser.msgBox('「' + targetCourseName + '」への招待を実行しました。');
  }
}

実行結果(既存メンバーの場合)

招待するクラスに既に参加しているメンバーの場合は下図のようになり、招待は実行されません。

実行結果(招待済みメンバーの場合)

招待するクラスへの招待を既に作成しているメンバーの場合は下図のようになり、招待は実行されません。

6. ドメイン外ユーザーを招待した際のエラー処理

招待した相手のドメインが招待の作成元(つまり自分)と違う場合、招待済みメンバーのデータが取得できないため別途エラー処理を作りたいと思います。
エラー処理にはTryCatch文を使用します。(詳細は後述)

createInvitation()に追記します。

function createInvitation() {

  const courses = listCourses();

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const mainSheet = ss.getSheetByName('名簿');

  const targetCourseName = mainSheet.getRange("E3").getValue();
  const targetCourse = selectCourse(courses, targetCourseName);

  if(targetCourse) {

    // 既存メンバーと招待済みメンバーを取得
    const existMembers =getExistMembers(targetCourse.id);
    const invitedMembers = getInvitedMembers(targetCourse.id);

    const members = getMembers();

    members.forEach(member => {

      if(member.role == '副担任'){
        var role = 'TEACHER';
      }
      else if(member.role == '生徒') {
        var role = 'STUDENT';
      }
      else{
        return;
      }

      if(existMembers.find(m=> m.emailAddress == member.mail)){
        Browser.msgBox('「' + member.mail + '」は既に参加しています。');
        return;
      }

      if(invitedMembers.find(m=> m.emailAddress == member.mail)){
        Browser.msgBox('「' + member.mail + '」は招待済みです。');
        return;
      }

      let invitationData = {
      'userId': member.mail,
      'courseId': targetCourse.id,
      'role': role
      };
    
      // try文で招待の作成を試行
      try {
        Classroom.Invitations.create(invitationData);
      }
      // エラーが発生した場合、エラー内容を渡してcatch文を実行
      catch (error) {
        // エラー内容が「招待が既にある」というものの場合
        if (error.details.message = "Requested entity already exists"){
          Browser.msgBox('「' + member.mail + '」は招待済みです。(ドメイン外)');
          return;
        }
        // それ以外の場合
        else {
          Browser.msgBox('「' + member.mail + '」への招待作成で予期しないエラーが発生しました。');
          return;
        }
      }
    });
    Browser.msgBox('「' + targetCourseName + '」への招待を実行しました。');
  }
}

TryCatch文とは:

試行と応答処理を行う構文で、GASではなくJavascriptに含まれる機能です。
主にエラーハンドリング(エラー処理)に用いられ、今回の例も同様です。
Tryで実行した内容でエラーが発生した場合、処理を止めることなくCatchにそのエラー内容を渡して処理を実行します。
今回の例では、渡されたエラー内のメッセージが作成済み招待を指す内容だった場合、それに応じたメッセージボックスを表示するようにしています。
また、上記に該当しないエラーメッセージの場合は予期しないエラーとしてメッセージボックスを表示するようにしておきました。

実行結果

招待済みの相手のドメインが招待の作成元(つまり自分)と違う場合は下図のようになり、招待は実行されません。

まとめ

今回はGoogle Classroomのクラスへの招待を行うGASの改良として、既存メンバーや招待済みメンバーに対して招待を実行しない処理の追加をご紹介しました。
これでもしも重複して招待を作ろうとしてもエラーが発生せずに実行ができるようになりました。

一連のGASは関数や連動も多く、混乱する部分もあったかと思うので図に整理してみました。
スプレッドシート上でボタンを押下するとそれに対応した関数が実行されます。
「招待実行ボタン」でcreateInvitation()関数を実行すると他の関数も順次実行され、取得や判定等を経て招待が実行される作りになっています。

Google Classroomのクラスへの招待を行うGASはこれで完成です。
GASでできるGoogle Classroom機能の自動化は他にも色々とあるので、今後紹介していけたらと思います。

Google Workspaceの導入は当社にお任せください

ITディストリビューターであるシネックスジャパンはGoogle Cloud™ Partner Award を受賞するなど、長年にわたりGoogle™のグローバル認定ディストリビューターとして、総合的な Googleソリューションを提供しています。お客様にとって最適なソリューションの提案や導入、活用をサポートします。

製品・サービスについてのお問合せ

情報収集中の方へ

導入事例やソリューションをまとめた資料をご提供しております。

資料ダウンロード
導入をご検討中の方へ

折り返し詳細のご案内を差し上げます。お問い合わせお待ちしております。

お問い合わせ