Skip to content

Instantly share code, notes, and snippets.

@ochilab
Created August 5, 2026 05:32
Show Gist options
  • Select an option

  • Save ochilab/e270f91fe422877528fbf78994c5513f to your computer and use it in GitHub Desktop.

Select an option

Save ochilab/e270f91fe422877528fbf78994c5513f to your computer and use it in GitHub Desktop.
Classroomの課題URLから提出状況一覧をとってくる
function exportClassroomSubmissionStatus() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const classroomUrl = String(sheet.getRange('B1').getValue()).trim();
if (!classroomUrl) {
throw new Error('セルB1にGoogle Classroomの課題URLを入力してください。');
}
// URLからAPI上の正式なcourseIdとcourseWorkIdを特定
const target = findCourseWorkByUrl(classroomUrl);
const courseId = target.course.id;
const courseWorkId = target.courseWork.id;
const course = target.course;
const courseWork = target.courseWork;
const students = getAllStudents(courseId);
const submissions = getAllSubmissions(courseId, courseWorkId);
const submissionMap = new Map();
submissions.forEach(function (submission) {
submissionMap.set(String(submission.userId), submission);
});
const headers = [
'ユーザーID',
'氏名',
'メールアドレス',
'提出状況',
'Classroom状態',
'提出日時',
'更新日時'
];
const rows = students.map(function (student) {
const userId = String(student.userId);
const profile = student.profile || {};
const name = profile.name || {};
const submission = submissionMap.get(userId);
const state = submission
? submission.state
: 'NO_SUBMISSION_DATA';
return [
userId,
name.fullName || '',
profile.emailAddress || '',
convertSubmissionStatus(state),
state,
getTurnedInTime(submission),
submission && submission.updateTime
? new Date(submission.updateTime)
: ''
];
});
sheet.clearContents();
sheet.getRange('A1').setValue('コース名');
sheet.getRange('B1').setValue(course.name || '');
sheet.getRange('A2').setValue('課題名');
sheet.getRange('B2').setValue(courseWork.title || '');
sheet.getRange('A3').setValue('課題URL');
sheet.getRange('B3').setValue(classroomUrl);
sheet.getRange('A4').setValue('courseId');
sheet.getRange('B4').setValue(courseId);
sheet.getRange('A5').setValue('courseWorkId');
sheet.getRange('B5').setValue(courseWorkId);
sheet.getRange('A7:G7').setValues([headers]);
if (rows.length > 0) {
sheet.getRange(8, 1, rows.length, headers.length)
.setValues(rows);
sheet.getRange(8, 6, rows.length, 2)
.setNumberFormat('yyyy/MM/dd HH:mm:ss');
}
sheet.getRange('A7:G7')
.setFontWeight('bold');
sheet.setFrozenRows(7);
sheet.autoResizeColumns(1, headers.length);
Logger.log('courseId: ' + courseId);
Logger.log('courseWorkId: ' + courseWorkId);
Logger.log('学生数: ' + students.length);
Logger.log('提出情報数: ' + submissions.length);
}
/**
* Classroomの課題URLから、API上の正式なコースと課題を探す。
*/
function findCourseWorkByUrl(classroomUrl) {
const normalizedTargetUrl = normalizeClassroomUrl(classroomUrl);
const urlInfo = parseClassroomWebUrl(classroomUrl);
const courses = getAllCourses();
Logger.log('取得できたコース数: ' + courses.length);
Logger.log('URL内のコース識別子: ' + urlInfo.courseUrlId);
Logger.log('URL内の課題識別子: ' + urlInfo.courseWorkUrlId);
for (const course of courses) {
if (!isTargetCourse(course, urlInfo, normalizedTargetUrl)) {
continue;
}
Logger.log(
'候補コース: ' + course.name +
' / API ID: ' + course.id +
' / URL: ' + course.alternateLink
);
const courseWorks = getAllCourseWork(course.id);
for (const courseWork of courseWorks) {
if (isTargetCourseWork(
courseWork,
urlInfo,
normalizedTargetUrl
)) {
return {
course: course,
courseWork: courseWork
};
}
}
}
throw new Error(
'指定されたURLに対応する課題をClassroom APIから特定できませんでした。\n' +
'次の点を確認してください。\n' +
'・スクリプトを実行しているアカウントが、そのクラスの教師である\n' +
'・B1が課題の詳細画面URLである\n' +
'・別のGoogleアカウント用の /u/0/ などがURLに含まれていても問題ありません\n' +
'・課題が削除済み、またはアクセス権のない課題ではない'
);
}
/**
* 実行ユーザーがアクセスできる全コースを取得する。
*/
function getAllCourses() {
const courses = [];
let pageToken = null;
do {
const options = {
pageSize: 100,
courseStates: [
'ACTIVE',
'ARCHIVED',
'PROVISIONED',
'DECLINED',
'SUSPENDED'
]
};
if (pageToken) {
options.pageToken = pageToken;
}
const response = Classroom.Courses.list(options);
if (response.courses) {
courses.push.apply(courses, response.courses);
}
pageToken = response.nextPageToken || null;
} while (pageToken);
return courses;
}
/**
* 指定コースの全課題を取得する。
*/
function getAllCourseWork(courseId) {
const courseWorks = [];
let pageToken = null;
do {
const options = {
pageSize: 100,
courseWorkStates: [
'PUBLISHED',
'DRAFT',
'DELETED'
]
};
if (pageToken) {
options.pageToken = pageToken;
}
const response = Classroom.Courses.CourseWork.list(
courseId,
options
);
if (response.courseWork) {
courseWorks.push.apply(
courseWorks,
response.courseWork
);
}
pageToken = response.nextPageToken || null;
} while (pageToken);
return courseWorks;
}
/**
* URLに含まれるClassroom画面上の識別子を抽出する。
*/
function parseClassroomWebUrl(url) {
const decodedUrl = decodeURIComponent(url);
const courseMatch = decodedUrl.match(/\/c\/([^/?#]+)/);
const courseWorkMatch = decodedUrl.match(/\/a\/([^/?#]+)/);
if (!courseMatch) {
throw new Error(
'URLからコース部分を取得できませんでした。' +
'課題詳細ページのURLを指定してください。'
);
}
if (!courseWorkMatch) {
throw new Error(
'URLから課題部分を取得できませんでした。' +
'課題詳細ページのURLを指定してください。'
);
}
return {
courseUrlId: courseMatch[1],
courseWorkUrlId: courseWorkMatch[1]
};
}
/**
* 対象コースか判定する。
*/
function isTargetCourse(course, urlInfo, targetUrl) {
if (!course) {
return false;
}
if (String(course.id) === String(urlInfo.courseUrlId)) {
return true;
}
if (!course.alternateLink) {
return false;
}
const alternateLink = normalizeClassroomUrl(
course.alternateLink
);
if (alternateLink === targetUrl) {
return true;
}
return alternateLink.indexOf(
'/c/' + urlInfo.courseUrlId
) !== -1;
}
/**
* 対象課題か判定する。
*/
function isTargetCourseWork(courseWork, urlInfo, targetUrl) {
if (!courseWork) {
return false;
}
if (
String(courseWork.id) ===
String(urlInfo.courseWorkUrlId)
) {
return true;
}
if (!courseWork.alternateLink) {
return false;
}
const alternateLink = normalizeClassroomUrl(
courseWork.alternateLink
);
if (alternateLink === targetUrl) {
return true;
}
return alternateLink.indexOf(
'/a/' + urlInfo.courseWorkUrlId
) !== -1;
}
/**
* アカウント番号やクエリ文字列を除いてURLを比較しやすくする。
*/
function normalizeClassroomUrl(url) {
return String(url)
.trim()
.replace(/\/u\/\d+\//, '/')
.replace(/[?#].*$/, '')
.replace(/\/+$/, '');
}
function getAllStudents(courseId) {
const students = [];
let pageToken = null;
do {
const options = {
pageSize: 100
};
if (pageToken) {
options.pageToken = pageToken;
}
const response = Classroom.Courses.Students.list(
courseId,
options
);
if (response.students) {
students.push.apply(students, response.students);
}
pageToken = response.nextPageToken || null;
} while (pageToken);
return students;
}
function getAllSubmissions(courseId, courseWorkId) {
const submissions = [];
let pageToken = null;
do {
const options = {
pageSize: 100
};
if (pageToken) {
options.pageToken = pageToken;
}
const response =
Classroom.Courses.CourseWork.StudentSubmissions.list(
courseId,
courseWorkId,
options
);
if (response.studentSubmissions) {
submissions.push.apply(
submissions,
response.studentSubmissions
);
}
pageToken = response.nextPageToken || null;
} while (pageToken);
return submissions;
}
function convertSubmissionStatus(state) {
switch (state) {
case 'TURNED_IN':
return '提出済み';
case 'RETURNED':
return '提出済み・返却済み';
case 'CREATED':
case 'NEW':
return '未提出';
case 'RECLAIMED_BY_STUDENT':
return '提出取り消し・未提出';
case 'NO_SUBMISSION_DATA':
return '提出情報なし';
default:
return '要確認';
}
}
function getTurnedInTime(submission) {
if (
!submission ||
!submission.submissionHistory
) {
return '';
}
const turnedInTimes = submission.submissionHistory
.filter(function (history) {
return history.stateHistory &&
history.stateHistory.state === 'TURNED_IN' &&
history.stateHistory.stateTimestamp;
})
.map(function (history) {
return new Date(
history.stateHistory.stateTimestamp
);
});
if (turnedInTimes.length === 0) {
return '';
}
turnedInTimes.sort(function (a, b) {
return b.getTime() - a.getTime();
});
return turnedInTimes[0];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment