1
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Google Classroom APIを利用してGASで課題を投稿するための3ステップとTips

Last updated at Posted at 2021-10-14

#背景

職場で利用しているGoogle Classroomの作業には少々面倒臭いところがあります……。

その作業内容とは下記の各設定を指定して複数の課題をまとめて投稿することなのですが、
UIの操作性やクラスごとにページを切り替えたりする手間が馬鹿にできません。

  • クラス
  • トピック
  • タイトル
  • 投稿日時
  • 期限日時
  • 添付ファイル
  • etc.

RESTAPIであるClassroomAPIを使えば簡単に自動化できるかもしれないと夢見て、GASを書くことにしました。

#目的

スプレッドシートを活用して、
Classroomを開くことなく、
上記の設定を指定した課題を投稿する

#この記事を読んで得られる知見

  • ClassroomAPIを利用するための基本的な情報
  • Googleで使われる時間フォーマットRFC3339 timestampの変換方法
  • ClassroomAPIの注意点(下記で登場する※のところが要注意)

#環境・使用ツール

  • GASスクリプトエディタ(新しくなってそこそこ使いやすいです)
  • GitHub(GAS GitHubアシスタントを使うことで簡単にリポジトリを管理できます)
  • ClassroomAPI(リファレンス、GASにはClassroomドキュメントをGoogle拡張サービスから追加しておきましょう)
  • 日付操作用のライブラリDay.js(追加方法

#課題を投稿するまでの3ステップ

  1. クラスIDを取得
  2. トピックIDを取得
  3. 各設定を指定して、課題投稿

APIを利用するタイミングは3回ありましたので、各ステップごとに解説していきます。

#1. クラスIDを取得
残念ながら投稿に必要なクラスIDはUIから確認することができないようです。
リファレンスからAPI叩けるので、そちらから試すか、下記の方法で取得することができます。
まず、ログインしているGoogleアカウントで参加しているクラスの一覧を呼び出し、クラスIDを取得します。

// オプションの設定
let optionalArgs = {
  pageSize: 20
};
//  リストを取得
const response = Classroom.Courses.list(optionalArgs);
const courses = response.courses;

Courses.listというメソッドにコールします。
その際に、courseStates, pageSize, pageToken, studentId, teacherIdなどのオプションをパラメータとして追加可能です。
ここでは例として取得数を指定してみました。

※UIではクラスと表記されていますが、リファレンス上では全てcourseと表記されています
ややこしいので日本語ではクラス、英語ではcourseと書き分けています

{
  "id": string,
  "name": string,
  "section": string,
  "descriptionHeading": string,
  "description": string,
  "room": string,
  "ownerId": string,
  "creationTime": string,
  "updateTime": string,
  "enrollmentCode": string,
  "courseState": enum (CourseState),
  "alternateLink": string,
  "teacherGroupEmail": string,
  "courseGroupEmail": string,
  "teacherFolder": {
    object (DriveFolder)
  },
  "courseMaterialSets": [
    {
      object (CourseMaterialSet)
    }
  ],
  "guardiansEnabled": boolean,
  "calendarId": string
}

レスポンスで受け取るcoursesのフィールドはこのようになっています。
今回必要なのは名前とクラスIDだけなので、こちらをスプレッドシートに表示していきます。

if (courses && courses.length > 0) {
    // 取得したデータを名前とIDの形に整形
    let data = Object.keys(courses).map((k)=>[courses[k].name, courses[k].id]);
    // 出力欄を初期化して、シートに書き込み
    const url = ""; // 操作元のURL
    const sheetName = ""; // 操作元のシート名
    const sheet = SpreadsheetApp.openByUrl(url).getSheetByName(sheetName);
    sheet.getRange(*,*,*,*).clearContent();
    sheet.getRange(*,*,*,*).setValues(data);
}

Object型のメソッドを利用して2次元配列に変換し、スプレッドシートに貼り付けています。

#2. トピックIDを取得

投稿に必須ではありませんが、トピックの指定が必要な場合はトピックIDも取得する必要があります。
こちらもリファレンスからAPI叩けるので、そちらから試すか、下記の方法で取得することができます。
先ほど呼び出したクラスIDを使用して、トピックIDを取得します。

const courseId = sheet.getRange(*,*).getValue();
let optionalArgs = {
  pageSize: 10
};
const topics = Classroom.Courses.Topics.list(courseId, optionalArgs).topic;

Courses.Topics.listというメソッドにコールします。
その際に、courseIdとして先ほど呼び出してスプレッドシートに記録したクラスIDと、pageSize, pageTokenなどのオプションをパラメータとして追加可能です。
ここでは例として取得数を指定してみました。

{
  "courseId": string,
  "topicId": string,
  "name": string,
  "updateTime": string
}

レスポンスで受け取るCourses.topicsのフィールドはこのようになっています。
今回必要なのは名前とトピックIDだけなので、こちらをスプレッドシートに表示していきます。

if (topics && topics.length > 0) {
  // 取得したデータを名前とIDの形に整形
  let data = Object.keys(topics).map((k)=>[topics[k].name, topics[k].topicId]);

  // 出力欄を初期化して、シートに書き込み
  sheet.getRange(*,*,*,*).clearContent();
  sheet.getRange(*,*,*,*).setValues(data);
}

Object型のメソッドを利用して2次元配列に変換し、スプレッドシートに貼り付けています。

#3. 各パラメータを設定して、課題投稿

ようやく課題の投稿に入ります。
ここまでで取得したIDを含めて、リクエスト用のJSONフィールドを設定していきます。
時間の指定が少々ややこしいので、下記のように指定することにして、詳しいフォーマットを確認していきます。

  • 投稿日 :2021年10月15日
  • 投稿時刻:12時34分
  • 期限時刻:23時45分
// IDの取得
const courseId = sheet.getRange(*,*).getValue();
const topicId = sheet.getRange(*,*).getValue();

// タイトルの設定
const title = "";

// 投稿日の設定
let date = new Date(Date.UTC(2021, 9, 15));
let scheduledDate = date;
// 投稿時刻の設定
scheduledDate.setHours(12);
scheduledDate.setMinutes(34);
let scheduledTime = dayjs.dayjs(scheduledDate).toISOString();

// 期限時刻の設定
let dueTime = date;
dueTime.setHours(23-9);
dueTime.setMinutes(45);

// 添付ファイルの設定
let attachedFile = ""; // URLの形
let attachedFileTitle = "";

// パラメータの設定
const courseWork = {
  title: title,
  dueDate: { year: dueTime.getFullYear(), month: dueTime.getMonth()+1, day: dueTime.getDate() },
  dueTime: { hours: dueTime.getHours(), minutes: dueTime.getMinutes(), seconds: 0, nanos: 0 },
  state: "DRAFT",
  workType: "ASSIGNMENT",
  topicId: topicId,
  scheduledTime: scheduledTime
}
if (attachedFile){
  courseWork["materials"] = [{ 
    "link": {
      "url":attachedFile,
      "title":attachedFileTitle
    }        
  }];
}
try {
  // 課題の予約投稿
  let response = Classroom.Courses.CourseWork.create(courseWork, courseId);
  let resultUrl = response.alternateLink;
} catch(e) {
  // 失敗した時の処理
}

呼び出し済みのIDとタイトルについては大丈夫でしょう。

投稿日と締切日は今回同日としているので、
let date = new Date(Date.UTC(2021, 9, 15));
という形で設定します。
※月だけ0-indexなので、指定する月から1を引いた数を設定しましょう

問題は時刻指定の部分です。

// 投稿時刻の設定
let scheduledDate = date;
scheduledDate.setHours(12);
scheduledDate.setMinutes(34);
let scheduledTime = dayjs.dayjs(scheduledDate).toISOString();

setHourssetMinutesで時刻を設定します。
ただ、投稿日時のフォーマットはなんとタイムスタンプで指定されています。

string (Timestamp format)
Optional timestamp when this course work is scheduled to be published.
A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional digits. Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z".
引用:公式リファレンス

こちらで指定されているRFC3339 UTC "Zulu" formatというフォーマットに変換するにするのに一番手間取りました。
結論として、Day.jsというMoment.jsの後継ライブラリを利用してISO8601に変換しています。

一方で、期限時刻は指定されたフォーマットが異なっており、setHourssetMinutesで時刻を設定したものをそのまま活用できます。
ただし、時差9時間分を計算しておきましょう。

添付ファイルはURLとタイトルを設定しておきます。

{
  "courseId": string,
  "id": string,
  "title": string,
  "description": string,
  "materials": [
    {
      object (Material)
    }
  ],
  "state": enum (CourseWorkState),
  "alternateLink": string,
  "creationTime": string,
  "updateTime": string,
  "dueDate": {
    object (Date)
  },
  "dueTime": {
    object (TimeOfDay)
  },
  "scheduledTime": string,
  "maxPoints": number,
  "workType": enum (CourseWorkType),
  "associatedWithDeveloper": boolean,
  "assigneeMode": enum (AssigneeMode),
  "individualStudentsOptions": {
    object (IndividualStudentsOptions)
  },
  "submissionModificationMode": enum (SubmissionModificationMode),
  "creatorUserId": string,
  "topicId": string,

  // Union field details can be only one of the following:
  "assignment": {
    object (Assignment)
  },
  "multipleChoiceQuestion": {
    object (MultipleChoiceQuestion)
  }
  // End of list of possible types for union field details.
}

これまで設定した値をフィールドに付与していきます。
上記のように、課題(courseWork)のフィールドはたくさんありますが、"state""workTipe""dueDate, dueTime"には特有のフォーマットを指定しなければいけません。
特に、"state"は予約投稿の場合は"DRAFT"を指定します。
※即時投稿の場合は"scheduledTime"を設定せず、"state""PUBLISHED"にします

これらが終われば、後はリクエストを投げるだけです。

// 課題の予約投稿
let response = Classroom.Courses.CourseWork.create(courseWork, courseId);
let resultUrl = response.alternateLink;

レスポンスで受け取るのは課題(courseWork)のインスタンスなので、上記のフィールドがほぼそのまま帰ってきます。

※即時投稿では"alternateLink"(投稿された課題のURL)が生成されますが、予約投稿では"alternateLink"は生成されません

以上の流れで、responseが無事帰ってきたらミッションコンプリートです。
おめでとうございます。

#最後に
Classroom APIにはリファレンスには色々とハマりやすい罠がありました。

  • 予約投稿するやり方が明記されておらず、日時の指定フォーマットがややこしい
  • 投稿した時点でリンクは生成されているはずなのに、予約登校の時はそのURLを取得できない
  • クラスや課題を削除するメソッドとしてdeleteと書かれているのに、実際はremove

今後APIを使おうと思い立った誰かにとって、少しでもこの記事が役立つことを祈ります。

#参考資料

1
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?