📚

AppSheet+GAS+GoogleTaskで復習タスク作成アプリを作ってみた

2021/09/02に公開

経緯

僕は以前からプログラミング学習(PythonやらGASやら)をやっているのですが、いかんせんちょっとやっては飽きてほっぽりだし、またしばらく経ってからまた勉強する、の繰り返しのせいで全く身についていませんでした。

なので、今回はちゃんと復習してスキルを身に着けたいなと思い、今回復習タスク作成アプリを作成しました。

参考にさせていただいたサイト

https://www.virment.com/use-google-tasks-from-script/
https://www.yoshidumi.co.jp/collaboration-lab/google_appsheet_column02/

概要

エビングハウスの忘却曲線に基づき、学習した日から数えて翌日、3日後、1週間後、1ヶ月後に復習するタスクをGoogleTaskに追加するアプリです。エビングハウスの忘却曲線については下記リンクを参照。

https://ryugaku-kuchikomi.com/blog/ebbinghaus-the-forgetting-curve/

できたもの

https://youtu.be/FfN0RXZgl8w

使用ツール

AppSheet

アプリのフロントエンド側を担当。巷で話題のノーコードでアプリをサクサク作れるので、ノンプログラマーの僕でも非常にかんたんに作成することができました。
本来はAppSheetだけでもTodoアプリは作れるのですが、UIがより優れているGoogleTaskをどうしても使いたかったので、今回は単なる入力、編集用の窓口として利用することにしました。

SpreadSheet

アプリのDB。AppSheet側で作成、登録した学習内容と、学習内容に対応するGoogleTaskのTaskIDを管理します。

GoogleAppScript

アプリのバックエンド側の処理を担当。
SpreadSheetに記録された学習内容をGoogleTaskに連携し、復習タスクの新規作成、編集、削除を実施します。

GoogleTask

実際に復習タスクを確認するツール。
機能はシンプルだが、使い勝手は抜群。特に素晴らしいのはPCでの操作。自分はタスク登録をPCで行うのですが、他のツールではどこか操作に引っかかりを感じていました。ですがGoogleTaskはタスク名を打ち込んでEnterキーを押せばタスク作成が完結します。しかもそれが連続で流れるように行えるので、タスク作成にストレスを全く感じません。さすがGoogle。

構成

AppSheetを入力I/Fとして、いろいろ中継してGoogleTaskにアクセスしています。

タスク新規登録時のフローを例にとると、
1.AppSheetで作成したアプリを使ってその日の学習内容をSpreadSheetに記録する。
2.SpreadSheetへの記録をトリガーに、GAS側でTasksAPIにアクセスし、復習タスクを新規登録。TasksAPIからは作成した復習タスクのIDが返される。
3.復習タスクのIDをSpreadSheetに保存
の流れで動作します。

作り方

1.事前準備

1-1:GoogleSpreadSheetでタスク登録用のシートを作成

新規スプレッドシートを作成し、適当な名前をつけます。
シートは2枚作成します。
1枚目のシートの1行目の各セルに、「UniqueID」、「日時」、「タイトル」、「内容」、「タスク作成有無」と記入。シート名をstudyに変更します。

2枚目のシートの1行目の各セルに、「復習1回目」~「復習4回目」と記入。
シート名をtaskに変更します。

1-2:GoogleTaskで学習用リストを作成

GoogleTaskから、新規タスクリストを作成します。

1-3:GoogleAppScriptで作成した学習用リストのIDを取得

GoogleTaskの既存タスクリストを操作するには当該タスクリストのIDが必要であるため、GASでTasks APIを叩いて確認します。

先ほど作成したSpreadSheetから、ツール>スクリプトエディタを選択し、 GoogleAppScriptのエディタを開きます。

左タブのサービスから「Tasks API」を選択し、追加します。

その後、以下スクリプトを入力し、実行します。

function showTaskLists() {
  let taskLists = Tasks.Tasklists.list().getItems();
  if (!taskLists) {
    return [];
  }
  let taskList = taskLists.map(function (taskList) {
    return {
      id: taskList.getId(),
      name: taskList.getTitle()
    };
  });

  console.log(taskList);
}

初回実行時、アクセス承認を行う必要があるので、全て許可します。

実行が完了すると、実行ログに自分のアカウントで作成しているタスクリストの一覧が表示されるため、先ほど作成したタスクリストのIDを控えておきます。

2.AppSheetで学習内容を登録するアプリを作成

先ほど作成したSpreadSheetを元に、AppSheetで管理用アプリを作成します。

2-1:アプリを新規作成

AppSheetのサイトにアクセスし、「Make a new app」を選択。

「Create a new app」というダイアログが表示されるので、「Start with your own data」を選択します。

Choose your dataを選択し、先ほど作成したSpreadSheetを選択します。

これでもうアプリの雛形は完成です。

2-2:アプリの設定を修正

アプリの設定を修正していきます。

まずData>ColumnからStudyの欄を選択

色々設定がありますが、下記のように変更します。

次に見た目を修正します。見た目の修正は、UX>Viewsで調整できます。
選択後、Primary Viewsからstudyを選択します。

今回はView typeをtableに変更します。が、各自好きな見た目にしていただいて問題ありません。

試しにこの状態でプレビューの右下のプラスボタンを押して、データを新規作成してみます。
日時、タイトル、内容の3項目だけ入力できるようになっていればOKです。

保存すると以下のように入力内容が記録されます。

データ投入後、Spreadsheet側を見てみましょう。
投入したデータがstudyシートの各行に登録されているのが確認できます。

データ削除は入力したデータの詳細画面に遷移した後、右上のゴミ箱アイコンから削除できます。ゴミ箱アイコンをタップして、テストデータを削除しておいてください。

以上でAppSheet側の設定は完了です。あとはスマホにAppSheetのアプリ入れたらもう使えます。
が、このままではただ単にSpreadsheetにデータを投入するだけのアプリになってしまうので、ここからGASを使ってGoogleTaskへの連携機能を作っていきます。

3.GoogleAppScriptでバックエンドの処理を記述

以下のコードを1-3のときに使ったスクリプトエディタへ貼り付けてください。
【1-3で確認したタスクリストID】は各自書き換えをお願いします。

function discernTaskOperation(e) {

  //学習リスト用のタスクリストIDを指定
  const taskListID = '【1-3で確認したタスクリストID】'

  //エビングハウスの忘却曲線
  //ebbinghaus[i][0]→復習する日付+1
  //ebbinghaus[i][1]→何回目の復習か
  const ebbinghaus = [[2, 1], [4, 2], [8, 3], [31, 4]];

  const sSht = e.source;
  const studysht = sSht.getActiveSheet();
  const shtName = studysht.getName();

  if (shtName === "study") {

    //操作されたセルから編集行を特定
    const activeRng = studysht.getActiveRange();
    const activeRow = activeRng.getRow();
    const targetline = studysht.getRange(activeRow, 1, 1, 5).getValues();

    //studyシートの編集行からデータを取得
    const [id, date, title, notes, status] = targetline[0];

    //statusが未完了なら復習タスクを新規作成
    if (status == "未完了") {

      addNewStudyTask(taskListID, ebbinghaus,activeRow,date, title, notes);

    } else {

      //studyシートの編集行と対応するtaskシートの行を取得
      const tasksht = sSht.getSheetByName("task");
      const targetTaskList = tasksht.getRange(activeRow, 1, 1, 4).getValues();

      //statusが完了(まだタスクが残っている)なら、編集行のdate,title,notesのいずれかが変化したと判断し、タスクの書き換えを行う
      if (status == "完了") {

        console.log(targetTaskList[0])
        changeStudyTask(taskListID, ebbinghaus,targetTaskList[0], date, title, notes);

      //statusが空なら、学習リストから対象の学習項目が削除されたと判断し、編集行と対応する各タスクの削除を行う
      } else if (status == '') {

        tasksht.getRange(activeRow, 1, 1, 4).clear();
        deleteTasks( taskListID, targetTaskList[0]);

      }

    }
  }
}

//タスクの新規作成(前処理)
function addNewStudyTask(taskListId, ebbinghaus,rownum,date,title,notes) {

  //生成した各タスクのidをtaskシート追記するための配列
  let taskidlist = [[]]

  //1. 現在のスプレッドシートを取得
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  //2. 現在のシートを取得
  const studysht = ss.getActiveSheet();

  for(const eb of ebbinghaus){
    let newDate, newTitle, newNotes;
    [newDate, newTitle, newNotes] = [new Date(date.toString()),title,notes];

    //日付をTasksのフォーマットに修正
    console.log(newDate);
    newDate.setDate(newDate.getDate() + eb[0]);
    let timezone = Session.getScriptTimeZone();
    let RFC3339format = "yyyy-MM-dd'T'HH:mm:ss.SXXX";
    newDate = Utilities.formatDate(newDate, timezone, RFC3339format);
    console.log(newDate);

    //タイトルを何回目の学習かわかるように修正
    newTitle = Utilities.formatString("【%d/4】%s", eb[1], newTitle);
    console.log(newTitle);

    //タスクを学習リストのTodoへ追加
    //addTaskは追加したタスクのidを返すので、taskidlistへ生成されたidを追加していく
    taskidlist[0].push(addTask(taskListId, newTitle, newDate, newNotes));
  }
  

  //タスク作成完了の旨をシート1に記載
  studysht.getRange(rownum, 5).setValue("完了")

  //taskシートにtaskidlistの中身(4回分のtaskid)を追記
  const tasksht = ss.getSheetByName("task")
  tasksht.getRange(rownum, 1, 1, taskidlist[0].length).setValues(taskidlist)
}

//タスクの新規作成(TasksAPIを叩く)
function addTask(taskListId, title, date, notes) {

  let task = {
    title: title, //  タイトル
    notes: notes, // メモ
    due: date // 締切日。日付のフォーマットはRFC3339に準拠
    //due: '2021-08-10T00:00:00.000+08:00' // 締切日。日付のフォーマットはRFC3339に準拠
  };
  task = Tasks.Tasks.insert(task, taskListId);

  // 新規に作成したタスクのIDを返す
  return task.id

}

//タスクの編集(前処理)
function changeStudyTask(taskListId, ebbinghaus,studyTaskList, date, title, notes) {

  for(const eb of ebbinghaus){
    console.log(date);
    let changeDate =  new Date(date.toString());
    console.log(`changeDate is ${changeDate}`)
    let changeNotes = notes;
    changeDate.setDate(changeDate.getDate() + eb[0]);
    let timezone = Session.getScriptTimeZone();
    let RFC3339format = "yyyy-MM-dd'T'HH:mm:ss.SXXX";
    changeDate = Utilities.formatDate(changeDate, timezone, RFC3339format);

    let changeTitle = Utilities.formatString("【%d/4】%s", eb[1], title);

    updateTask(taskListId, studyTaskList[eb[1]-1], changeDate, changeTitle, changeNotes);
  }
}

//タスクの編集(TaskAPIを叩く)
function updateTask(taskListID, taskID, date, title, notes) {

  //タスクリストIDとタスクIDを使って指定タスクの情報を取得、インスタンスを生成
  let tasks = Tasks.Tasks.get(taskListID, taskID)
  console.log(tasks);

  //取得したタスクに対し、変更後の日付、タイトル、詳細を代入
  tasks.due = date;
  tasks.title = title;
  tasks.notes = notes;
  console.log(tasks);

  //Tasks.updateメソッドで、生成したtasksインスタンスの状態を抽出元のtaskへ反映する
  Tasks.Tasks.update(tasks, taskListID, taskID);

}

//タスクの削除
function deleteTasks(taskListId, targetTaskList) {

  targetTaskList.forEach(function (delTask) {
    Tasks.Tasks.remove(taskListId, delTask);
  });

}

その後、エディタ左のツールバーからトリガーを選択し、以下の通り設定します。
1-3のスクリプト実行時と同様に承認を行う必要があるので、同様にすべて承認してください。

コードの内容について補足すると、discernTaskOperation()の始めにあるebbinghausの内容はそれぞれebbinghaus[i][0]が学習日から数えて何日後に復習するか、 ebbinghaus[i][1]が何回目の復習か、という意味です。ただし実際はebbinghaus[i][0]は復習する日にち+1の値となっているため、復習日時を変更したい時はそこだけ注意してください。

処理の流れとしては、Spreadsheetの変更をトリガーとして、5行目の「タスク作成有無」の状態に応じて以下の通り処理を変更します。

以上で学習履歴管理アプリの作成は終了です。お疲れさまでした。

終わりに

AppSheetはそれ単体でも業務に活用できる様々なアプリを作成することができますが、GASを組み合わせるとより複雑なデータ操作も可能になります。アイデア次第でかんたんに便利なアプリを作成できますので、皆さんも使ってみてください。

Discussion