Skip to content

Instantly share code, notes, and snippets.

@Tomokatsu-Sakamoto
Last active January 21, 2023 23:57
Show Gist options
  • Save Tomokatsu-Sakamoto/3c5b6f92a03b9dbd7676d5d47140045e to your computer and use it in GitHub Desktop.
Save Tomokatsu-Sakamoto/3c5b6f92a03b9dbd7676d5d47140045e to your computer and use it in GitHub Desktop.
組織内で作成されている Google Classroom のクラスの一覧を作成する GAS のプログラム。
"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