Last active
January 21, 2023 23:57
-
-
Save Tomokatsu-Sakamoto/3c5b6f92a03b9dbd7676d5d47140045e to your computer and use it in GitHub Desktop.
組織内で作成されている Google Classroom のクラスの一覧を作成する GAS のプログラム。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; // 変数の宣言を強要する | |
/** @OnlyCurrentDoc */ // 他のファイルにはアクセスしない | |
/** | |
* 2023/01/21 | |
* ・ownerId をそのまま表示するのではなく、可能であればメールアドレスに変換する | |
* ・「投稿」だけでなく、「課題」「資料」についても最新の日時を表示する | |
* ・一覧の作成後、カーソル行のクラスをアーカイブする機能をメニューに追加 | |
*/ | |
const WEBHOOK_SPACE = ''; // 終了通知のためのスペースの Webhook | |
const NEXT_TOKEN = 'NEXT_TOKEN'; | |
const EXEC_DATE = 'EXEC_DATE'; | |
const EXEC_COUNT = 'EXEC_COUNT'; | |
const EXEC_TOTAL = 'EXEC_TOTAL'; | |
const MAX_COUNT = 1000; | |
const MAX_MINUTES = 25; // 実行時間制限を回避するために中断するための時間 | |
let ID_TABLE = {}; // これまでに処理した内容 ※キャッシュとして連想配列を使用 | |
/** | |
* 引数に与えられた id を、メールアドレスに変換する | |
*/ | |
function id2email(id) { | |
let ret = ID_TABLE[id]; // 関数の変換結果として戻す内容 | |
if (ret == null) { // 連想配列に登録されていなければ API で取得する | |
try { | |
let res = Classroom.UserProfiles.get(id); | |
ret = res.emailAddress; | |
} | |
catch (e) { | |
console.log('Error! ' + e); | |
ret = id; | |
} | |
ID_TABLE[id] = ret; // 今回の結果を追加 | |
} | |
return ret; // 処理で得られた内容を戻す | |
} | |
/** | |
* 追加のメニュー項目を設定する | |
*/ | |
function onOpen() { | |
let ui = SpreadsheetApp.getUi(); | |
ui.createMenu('クラス一覧') | |
.addItem('クラス一覧の作成 ※シートの内容はクリアされます', 'start') | |
.addSeparator() | |
.addItem('カーソル行のクラスをアーカイブ', 'archiveCourse') | |
.addToUi(); | |
} | |
/** | |
* 選択されているカーソル行のクラスをアーカイブする | |
*/ | |
function archiveCourse() { | |
let sheet = SpreadsheetApp.getActiveSheet(); // 現在開いているスプレッドシート | |
let row = sheet.getActiveRange().getRow(); | |
let column = sheet.getActiveRange().getColumn(); | |
console.log("Active : " + row + "," + column); | |
if (row == 1) { // 先頭行は処理対象から外す | |
SpreadsheetApp.getActiveSpreadsheet().toast('この処理は、見出し行では実行できません。'); | |
} | |
else { | |
let id = sheet.getRange(row, 1).getValue(); | |
let name = sheet.getRange(row, 2).getValue(); | |
let iTeachers = sheet.getRange(row, 17).getValue(); // クラスに参加している教師の数 | |
let iStudents = sheet.getRange(row, 18).getValue(); // クラスに参加している生徒の数 | |
let result = Browser.msgBox( | |
'本当に ' + row + ' 行目のクラス「' + name + '」' + | |
'(教師 ' + iTeachers + '人、生徒 ' + iStudents + '人)' + | |
'をアーカイブしても構いませんか?', | |
Browser.Buttons.OK_CANCEL); | |
if (result == "ok") { | |
try { | |
// 参考: https://developers.google.com/classroom/reference/rest/v1/courses/patch | |
let cCourses = Classroom.Courses.patch( | |
{ | |
'courseState': "ARCHIVED" // 「アーカイブ」に設定 | |
}, | |
id, | |
{ | |
'updateMask': 'courseState' // 変更するのは、courseState のみ | |
} | |
); | |
let reWrite = [ | |
[ | |
cCourses.id, // Identifier for this course assigned by Classroom. | |
cCourses.name, // Name of the course. | |
cCourses.section, // Section of the course. | |
cCourses.descriptionHeading, // Optional heading for the description. | |
cCourses.description, // Optional description. | |
cCourses.room, // Optional room location. | |
id2email(cCourses.ownerId), // The identifier of the owner of a course. | |
cCourses.creationTime, // Creation time of the course. | |
cCourses.updateTime, // Time of the most recent update to this course. | |
cCourses.enrollmentCode, // Enrollment code to use when joining this course. | |
cCourses.courseState, // State of the course. | |
] | |
]; | |
sheet.getRange(row, 1, 1, reWrite[0].length).setValues(reWrite); // 更新後の内容で上書き | |
SpreadsheetApp.getActiveSpreadsheet().toast('指定されたクラスをアーカイブしました。'); | |
sheet.getCurrentCell().offset(1, 0).activate(); // 次の行に移動 | |
} | |
catch (e) { | |
console.log(e); | |
} | |
} | |
} | |
} | |
/*** | |
* 処理を開始する | |
*/ | |
function start() { | |
const sheet = SpreadsheetApp.getActiveSheet(); | |
PropertiesService.getScriptProperties().setProperty(EXEC_DATE, ""); | |
sheet.getDataRange().clearContent(); // 現在のシートの内容をクリア | |
sheet.appendRow( | |
[ | |
'id', | |
'name', | |
'section', | |
'descriptionHeading', | |
'description', | |
'room', | |
'ownerId', | |
'creationTime', | |
'updateTime', | |
'enrollmentCode', | |
'courseState', | |
'alternateLink', | |
'teacherGroupEmail', | |
'courseGroupEmail', | |
'guardiansEnabled', | |
'calendarId', | |
'Teacher', | |
'Student', | |
'Announcement', | |
'courseWork', | |
'courseWorkMaterial', | |
] | |
); | |
// 新規に処理を開始させるために、プロパティに保存されている内容をクリア | |
PropertiesService.getScriptProperties().setProperty(EXEC_DATE, ''); | |
PropertiesService.getScriptProperties().setProperty(EXEC_COUNT, ''); | |
PropertiesService.getScriptProperties().setProperty(EXEC_TOTAL, ''); | |
PropertiesService.getScriptProperties().setProperty(NEXT_TOKEN, ''); | |
batch(); | |
} | |
/*** | |
* 1分後に batch 関数を起動するトリガーを作成 | |
*/ | |
function createTrigger() { | |
ScriptApp.newTrigger("batch") | |
.timeBased() | |
.after(1 * 60 * 1000) // 起動するタイミングは 1分後(指定する時間は ms 単位) | |
.create(); | |
} | |
/*** | |
* 中断した処理を継続するためのトリガー関数 | |
*/ | |
function batch(e) { | |
let today = Utilities.formatDate(new Date(), 'JST', 'yyyy-MM-dd'); | |
getActiveClass(today); | |
} | |
/*** | |
* 継続実行のためのトリガー関数の設定を削除する | |
*/ | |
function deleteTriggers() { | |
const triggers = ScriptApp.getProjectTriggers(); | |
for (const trigger of triggers) { | |
// batch を実行するトリガーだった場合は削除する | |
if (trigger.getHandlerFunction() === "batch") ScriptApp.deleteTrigger(trigger); | |
} | |
} | |
/*** | |
* クラスの作成状況を書き出す | |
*/ | |
function getActiveClass(targetDate) { | |
const sheet = SpreadsheetApp.getActiveSheet(); | |
let execDate = PropertiesService.getScriptProperties().getProperty(EXEC_DATE); | |
let execCount = Number(PropertiesService.getScriptProperties().getProperty(EXEC_COUNT)); | |
let execTotal = Number(PropertiesService.getScriptProperties().getProperty(EXEC_TOTAL)); | |
let nextToken = PropertiesService.getScriptProperties().getProperty(NEXT_TOKEN); | |
let exitTime = new Date(); | |
let nowTime = new Date(); | |
exitTime.setMinutes(exitTime.getMinutes() + MAX_MINUTES); // 指定された時間だけ進めた時間 | |
if (execDate != targetDate) { // 実行日が変わっていれば、はじめからやり直す | |
console.log('はじめから実行する'); | |
execDate = targetDate; | |
execCount = 0; | |
execTotal = 0; | |
nextToken = null; | |
} | |
execCount++; | |
try { | |
let iCount = 0; | |
do { | |
iCount++; | |
console.log(`${execCount}回目:[${iCount}] ${execTotal}件 - ${targetDate} - ${nextToken}`); | |
// 組織内で作成されているクラスの一覧を取得する | |
// 参考: https://developers.google.com/classroom/reference/rest/v1/courses/list | |
let optionalArgs = { | |
pageSize: 100, | |
courseStates: 'ACTIVE', // アクティブなクラスだけを対象にする | |
pageToken: nextToken, // 継続して処理するためのトークンを設定 | |
}; | |
let res = Classroom.Courses.list(optionalArgs); | |
if (res.courses != null) { | |
for (let i = 0; i < res.courses.length; i++) { | |
// 参考: https://developers.google.com/classroom/reference/rest/v1/courses#Course | |
let cCourses = res.courses[i]; | |
let dateA = ''; // 最新の投稿の日付 | |
let dateW = ''; // 最新の課題の日付 | |
let dateM = ''; // 最新の資料の日付 | |
try { | |
// 参考: https://developers.google.com/classroom/reference/rest/v1/courses.announcements/list | |
let resA = Classroom.Courses.Announcements.list( | |
cCourses.id, | |
{ | |
pageSize: 1, // 最新だけ取得するために 1件だけ | |
} | |
); | |
if (resA.announcements) { // 投稿が存在していれば、作成日時を設定する | |
dateA = resA.announcements[0].creationTime; | |
} | |
} | |
catch (e) { | |
dateA = '---'; | |
} | |
try { | |
// 参考: https://developers.google.com/classroom/reference/rest/v1/courses.courseWork/list | |
let resW = Classroom.Courses.CourseWork.list( | |
cCourses.id, | |
{ | |
pageSize: 1, // 最新だけ取得するために 1件だけ | |
} | |
); | |
if (resW.courseWork) { // 課題が存在していれば、作成日時を設定する | |
dateW = resW.courseWork[0].creationTime; | |
} | |
} | |
catch (e) { | |
dateW = '---'; | |
} | |
try { | |
// 参考: https://developers.google.com/classroom/reference/rest/v1/courses.courseWorkMaterials/list | |
let resM = Classroom.Courses.CourseWorkMaterials.list( | |
cCourses.id, | |
{ | |
pageSize: 1, // 最新だけ取得するために 1件だけ | |
} | |
); | |
if (resM.courseWorkMaterial) { // 資料が存在していれば、作成日時を設定する | |
dateM = resM.courseWorkMaterial[0].creationTime; | |
} | |
} | |
catch (e) { | |
dateM = '---'; | |
} | |
let iTeachers = 0; // クラスに参加している教師の数 | |
let iStudents = 0; // クラスに参加している生徒の数 | |
// 参考: https://developers.google.com/classroom/reference/rest/v1/courses.teachers/list | |
let resT = Classroom.Courses.Teachers.list( | |
cCourses.id, | |
{ | |
pageSize: 100, | |
} | |
); | |
if (resT.teachers) { // 教師の数を取得 | |
iTeachers = resT.teachers.length; | |
} | |
let TokenSt = null; | |
do { | |
// 参考: https://developers.google.com/classroom/reference/rest/v1/courses.students/list | |
let resS = Classroom.Courses.Students.list( | |
cCourses.id, | |
{ | |
pageSize: 100, | |
pageToken: TokenSt, // 継続して処理するためのトークンを設定 | |
} | |
); | |
if (resS.students) { // 生徒の数を取得 ※一度で取得できない場合があるので、くり返し処理 | |
iStudents += resS.students.length; | |
} | |
TokenSt = resS.nextPageToken; | |
} while (TokenSt != null); | |
sheet.appendRow( | |
[ | |
cCourses.id, // Identifier for this course assigned by Classroom. | |
cCourses.name, // Name of the course. | |
cCourses.section, // Section of the course. | |
cCourses.descriptionHeading, // Optional heading for the description. | |
cCourses.description, // Optional description. | |
cCourses.room, // Optional room location. | |
id2email(cCourses.ownerId), // The identifier of the owner of a course. | |
cCourses.creationTime, // Creation time of the course. | |
cCourses.updateTime, // Time of the most recent update to this course. | |
cCourses.enrollmentCode, // Enrollment code to use when joining this course. | |
cCourses.courseState, // State of the course. | |
cCourses.alternateLink, // Absolute link to this course in the Classroom web UI. | |
cCourses.teacherGroupEmail, // The email address of a Google group containing all teachers of the course. | |
cCourses.courseGroupEmail, // The email address of a Google group containing all members of the course. | |
cCourses.guardiansEnabled, // Whether or not guardian notifications are enabled for this course. | |
cCourses.calendarId, // The Calendar ID for a calendar that all course members can see, to which Classroom adds events for course work and announcements in the course. | |
iTeachers, // 当該クラスの教師の数 | |
iStudents, // 当該クラスの生徒の数 | |
dateA, // 当該クラスの最新の投稿が作成された日時 | |
dateW, // 当該クラスの最新の課題が作成された日時 | |
dateM, // 当該クラスの最新の資料が作成された日時 | |
] | |
); | |
} | |
execTotal += res.courses.length; // 処理件数を加算 | |
} | |
nextToken = res.nextPageToken; // データが継続していれば null 以外が設定されているはず | |
if (nextToken != null) { | |
PropertiesService.getScriptProperties().setProperty(EXEC_DATE, String(execDate)); | |
PropertiesService.getScriptProperties().setProperty(EXEC_COUNT, String(execCount)); | |
PropertiesService.getScriptProperties().setProperty(EXEC_TOTAL, String(execTotal)); | |
PropertiesService.getScriptProperties().setProperty(NEXT_TOKEN, nextToken); | |
} | |
else { | |
PropertiesService.getScriptProperties().setProperty(EXEC_DATE, ''); | |
PropertiesService.getScriptProperties().setProperty(EXEC_COUNT, ''); | |
PropertiesService.getScriptProperties().setProperty(EXEC_TOTAL, ''); | |
PropertiesService.getScriptProperties().setProperty(NEXT_TOKEN, ''); | |
} | |
nowTime = new Date(); | |
if (exitTime <= nowTime) { | |
break; | |
} | |
} while ((nextToken != null) && (iCount < MAX_COUNT)); | |
} | |
catch (e) { | |
// The service is currently unavailable. | |
// が発生した場合には、時間を改めてリトライするしかないので、エラー終了させずに処理を継続させる | |
Logger.log(e); // 状況をログには残しておく | |
} | |
// 時間制限に抵触しないように中断したか? | |
if (nextToken != null) { | |
deleteTriggers(); | |
createTrigger(); // 継続して処理を実行するためのトリガーを設定 | |
} | |
else { // 一覧作成が終了したならば、最終行にその旨を追加する | |
console.log('クラスの一覧作成処理が終了しました。'); | |
sendChatMessage('クラスの一覧作成処理が終了しました。'); | |
sheet.appendRow(['End of classroom list.']); | |
} | |
} | |
/******************************************************************************** | |
* Webhook を用いて、スペースに投稿する。 | |
*/ | |
function sendChatMessage(content) { | |
const webhook = WEBHOOK_SPACE; | |
if (webhook != '') { // Webhook が指定されていなければ、投稿しない | |
let message = { // 送信内容作成 | |
'text': content // 単純にテキストメッセージだけを投稿する | |
}; | |
// スペースにメッセージを送信 | |
try { | |
UrlFetchApp.fetch( | |
webhook, | |
{ // UrlFetchApp の追加パラメータ | |
'method': 'POST', | |
'muteHttpExceptions': true, | |
'headers': { | |
'Content-Type': 'application/json; charset=UTF-8' | |
}, | |
'payload': JSON.stringify(message) | |
} | |
); // エラーを検出できるように、try で囲んでおく | |
} | |
catch (e) { | |
Logger.log(e); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment