memorandums

日々の生活で問題解決したこと、知ってよかったことなどを自分が思い出すために記録しています。

Google Classroomへの課題提出日時を取得したかったのです

まえおき

コロナ以降、Google Classroomを使うようになりました。今や提出物管理に欠かせない存在です。個人的にはあまり好きではありませんが(課題のビュアーが激遅)、とりあえず機能が揃っているので使っています。

プログラミング課題は提出期間が1週間くらい余裕がある場合が多いと思います。たぶんですが。で、演習日の当日にすぐ提出する人もいれば期限ギリギリに出す人もいます。どうやろうが学生さんの勝手なんですが気になるんですね。性分的に。

この「提出時期」は成績や理解度、もっというとモチベーションに関係しているんじゃないか、という仮説(というか思い込み)が私の中にあります。授業やバイトで忙しい中、空き時間を見つけて一生懸命取り組んだ結果、時間がかかった、という場合もあるでしょうし、授業中にやれる分はやったけど期限ギリギリまで何もせず慌てて何かしらの方法で取り組んで出した、という場合もあるでしょう。どの提出がどんな状態だったかはわかりません。でも、何か関係があると思うんですね。

余談ですが、昔、以下のような研究をしたことがあります。同じような動機からはじめました。プログラムを実行したときにファイルが保存されるのですが、その保存した回数をタイムスタンプつきで保存するツールを作ったという研究です。PDFが以下からダウンロードできますので興味があればどうぞ。

Eclipseのローカルヒストリーを用いた持ち帰り課題の取り組み状況分析ツール, 情報処理学会研究報告 コンピュータと教育(CE), 2018-CE-143(26)

結果の1つが以下です。横軸が提出期間中の授業日からの経過日数。縦軸が編集回数(=プログラムの実行回数)です。期限間際の活動量が多いですね。2018年と古いですが、課題の取り組み状況の傾向は現在も似たような感じなのかなと思っています。

話を戻します。

Google Classroomでは、各学生さんの課題の提出日時はわかります。ただ、1つ1つ課題を選択して開かないと見えないんです。課題提出一覧をダウンロードする機能はあるのですが提出日時は含まれていません。残念です。

ここはGAS(Google App Script)の出番だなと思いました。ググって調べてみました。

Google Classroomに対してGASを実行することは一般ユーザではできません。APIの使用にはワークスペースの管理者権限が必要になるようです。僕は平社員なのでできません。ここで諦めようと思ったのですが、考えてみれば、自分の担当する科目の提出物は自分のGoogle Driveに保存されるんです。保存場所はGoogle Driveの直下です。フォルダ名はズバリ Classroomです。その中に科目ごと課題ごとにフォルダが作られ、そのフォルダの中に学生さんが提出したファイルが保存されています。

試行1:DriveAppを使う(GAS)

このファイル一覧をGASで取得できればよさげです。ファイルごとのオーナーと日時をとれば一覧をゲットできるじゃんと思いました。早速ググると以下の記事がありました。ありがたいです。

Google Drive上の指定のフォルダ以下のファイル一覧をSpreadSheetに書き出す #GoogleAppsScript - Qiita

実際にやってみたのですが問題がありました。GASで取得したファイルのオーナーがなぜか、私になっている場合と学生さんになっている場合がありました。なぜこういうことが起きるのかわかりません。上記のサイトで紹介されているやり方はDriveAppというオブジェクトを使っていて、このClass関係で得られるファイル情報には学生のアカウント情報がなかったんですね。色々やりましたが省略します。

それでも、Google Driveのブラウザ表示には、オーナーの他に学生のアカウント名がちゃんと表示されているんです。DriveAppが対応していないだけで、データ構造的には、個々のファイルに提出した学生のアカウント名が関連づけられていることが推察されます。

試行2:Drive APIを使う(GAS)

ずーっと調べていくと上記のDriveAppとは別にDrive APIを利用したコードがありました。上記とこれにどこが違うのか調べていませんが。参考にさせていただいたのは以下の記事です。ありがとうございます🙇‍♂

【GAS】Google Driveの指定したフォルダ配下のファイル一覧を取得 - みやもとメモ

上記のサイトに掲載されているコードを簡略化して以下示します。

// フォルダID
const folderId = "フォルダIDを指定";
function main() {
  const option = {
    "orderBy": "title",
  }
  const files = Drive.Children.list(folderId, option);
  for (const file of files.items) {
    const fileData = Drive.Files.get(file.id);
    console.log(fileData);
  }

これを実行するには、一般的なGASと同様にGASファイルをGoogle Drive上に新規作成し、そこにこのコードをいれ、GASの編集画面の左にある「+サービス」でDrive APIを追加して実行すればできます。

上記のコードを実行すると、(consle.logのところで)取得可能な個々のファイル情報が得られます。この情報を眺めながら、欲しかった2つの情報が得られることがわかりました。以下、方法を書きます。

アカウント名

lastModifyingUserNameという属性にはいっていました👍 これでファイルの提出者を特定できます。

提出日時

日付関係の属性が以下の4種類ありました。調べたのでメモを残しておきます。ある課題のファイル全部について実行した結果、どのファイルにも含まれていたのはmodifiedDateとcreatedDateでした。それ以外は入っていないデータもありましたのでファイル日付としては使えないと思いました。

属性名 調べてわかったこと
modifiedDate 学生のDriveにファイルを最後に更新した日時が入っていると思われます
createdDate modifiedDateよりあとの日時が入っていました。つまり、学生がClassroomに投稿したときに講師のDriveに作成された時刻になるのではないかと
sharedWithMeDate createdDateよりあとの日時が入っていました。Classroomサービスが投稿のためファイルを作成しその後共有した日時かな?
markedViewedByMeDate UNIXタイムが入っていました。恐らく値は0なのでしょう。
lastViewedByMeDate 僕が見たファイルには日時が入っていました

できたコード

以上の調査からファイルの更新日としてはcreatedDateが使えそうなことがわかりました。で、色々試しながら以下のコードを書きました。上記の2つのコードを合体して作成しました。お二方、ありがとうございました。

// フォルダID
const folderId = "1WPXepYXErE6EEh-fffgxFOj34rc9uqAef9BKK8cqa5QJ4vpcauFIktXmVlmTINxXwyo3TlzU";

function main() {
  const option = {
    "orderBy": "title",
  }

  //ファイル情報を取得する
  let list = [];
  const files = Drive.Children.list(folderId, option);
  for (const file of files.items) {
    const f = Drive.Files.get(file.id);
//    console.log(f);
    list.push([f.lastModifyingUserName, Utilities.formatDate(new Date(f.createdDate), "JST", "MM/dd HH:mm:ss")]);
  }

  //複数回投稿している場合は最終更新時刻だけ残したいので、まずアカウント名でソートする
  list = list.sort();

  //重複している行を削除(日時の古⇒新に並んでいるので後ろを残す)
  list2 = []
  let i = 0, j = 0;
  while (i < list.length) {
    j = i;
    while (j < list.length && list[i][0] == list[j][0]) j++;
    list2.push(list[j-1]);
    i = j;
  }

  //スプレッドシートに書き出す
  let ss = SpreadsheetApp.getActive();
  let sheet = ss.getSheetByName("シート1");
  let range = sheet.getRange(1, 1, list2.length, list2[0].length);
  range.setValues(list2);
}

Googleスプレッドシートを新規作成し、GASを追加して、上記のコードをコピペします。あとは、コード中のfolderIdをご自身のIDに書き換えるだけです。folderIdは当該フォルダをGoogle Driveで開いているときに表示されるURLの最後の文字列です(https://drive.google.com/drive/u/1/folders/ココ!)。

このエントリーの目的としてはここまでで終了ですが、ちょっと以下補足まで書きますね。

補足メモ

このスクリプトでちょっと工夫しているところをメモします。1週間もすれば忘れるので。。。

Google Classroomでは学生さんが課題提出(ファイルアップロード)するたびにGoogle ClassroomがDrive上にファイルを新規作成します。この仕様もどうかと思うのですが、消すことはサービス側にとっては責任問題になるので、履歴も含めて保存しようという思想なのではないかと思います。なので、同じ学生から同じファイルでも複数回の提出があった場合は同じファイルが複数できちゃうんですね💦

そこで、ある課題に対して同一アカウントからの提出があった場合は最新以外を除外する処理を以下で行います。おわかりかと思いますが、これは課題に対して提出ファイルが1つだけだった場合のプログラムになります。もし1つの課題で1つのアカウントから複数(種類の)ファイルの提出が必要な場合は最新の1ファイルしか残りませんので大問題かと思います。僕の場合はそういう課題は滅多にないのでこれでOKなんですが(プログラムリストと実行結果を単一のレポートにまとめて出させているため)、もし利用される場合は注意してください。

  //複数回投稿している場合は最終更新時刻だけ残す
  list = list.sort();

  //重複している行を削除(日時の古⇒新に並んでいるので後ろを残す)
  list2 = []
  let i = 0, j = 0;
  while (i < list.length) {
    j = i;
    while (j < list.length && list[i][0] == list[j][0]) j++;
    list2.push(list[j-1]);
    i = j;
  }

Arrayには2つの値(アカウント名, 提出日時)が入っているのですが、sortメソッドを実行してみると、アカウント名を辞書順に並び替えてくれるのは当然ですが提出日時も一緒に並び替えてくれました。ありがたし。

あとは、「//重複している行を削除」で重複行を除外するコードを書きました。これ、別に珍しくも何ともないコードですが、実はちょうど今週の2年生向けの科目で扱った内容(連長圧縮)がそのまま使えたんですね。なんか嬉しくてこの説明を書き足すことにしました。

重複行を除外するコードはこれまで何度か書いた経験がありますが、結構、面倒だった記憶しかありません。最初もしくは最後の要素を特別扱いしなければならないなど、です。でも、今週知ったアルゴリズムを使うと本当にスッキリ書けるんですね。もっと早く勉強しておくべきでした。競プロは役に立ちます。

ちなみに、連長圧縮のアルゴリズムは以下のようになります。使用している参考書から引用しています。上記のコードはまさにそうなっていますよね。助かりました。ほんと。

S = "AAABBBBAACDD"
N = len(S)

# 長さNの文字列Sが与えられたとする
i = 0
while i < N:
    # 初めて S[j != S[i] となる場所を求める
    j = i
    while j < N and S[j] == S[i]:
        j += 1
    
    # 文字S[i]がj-i文字連続したと表示する
    print(S[i], j - i)

    # iをjのところへワープさせる
    i = j

扱った問題は、この本の通り、以下を扱いました。いや、僕自身がほんと勉強になっています。なかなか時間をとってこれをやるのが難しいですから。。。

F - DoubleCamelCase Sort

ついでに、参考書を以下に示しますね。とてもわかりやすく書かれています。これまでの競プロ本(これとかこれ)は気軽に読むには難しすぎて挫折しましたが、これはプログラミング経験と動機があれば読みさえすればわかるように書かれています。

ということで、おしまい。

提出日時から分析していきたいと思います。