CombConf を支える技術 〜タスクリマインダー〜

こんにちは

こんにちは!

CombConf

ご存知の方も多いと思いますが、今ぼくは12月23日に開催する中学生・高校生向けのカンファレンス、 CombConf の運営をやっています。 CombConf での肩書きはわくわくエンジニアです! わくわくしてます!

CombConf ってなによって方は、 CombConf 公式サイト や、 gihyo.jp の CombConf 紹介記事 をご覧ください!

中学生・高校生では無い方の参加も一般参加枠にてお待ちしています! CombConf 一般参加枠参加登録フォームよりどしどし参加登録してください!

Google Apps Script

ふと思い立って Google Apps Script で遊びたくなったので遊んでみました。 Google Apps Script とは、 Google Spreadsheet なんかを JavaScript で操作できるイカした奴です。 Google Apps Script についての詳しい説明は面倒ですし、ぼくもいじりはじめてから5時間くらいしか経ってなくてよく知らないので、 Google Apps Script - Google Developers とか読んでください。

Google Apps Script はこいつ単体で Cron とかメールの送信とかできちゃうデキる子です。

Google Apps Script Editor

Google Apps Script の開発環境として、 Google が公式に Web IDE を提供しているのですが、こいつは全くもって気が利きません。 こいつの気の利かない感じは、行頭でスペース1個打ってからタブ打ってみたり、タブ打ってから Backspace 打ってみたり、バグの無い関数に対してデバッガーを起動したりすれば実感していただけるのでは無いでしょうか。

そうそう、それから

function foo(){
}

は関数として認識してくれるのに、

var foo = function(){}

を関数として認識してくれない辺りにはイラッとしました。 名前がついた関数か、無名関数を代入された変数かの違いがあることは認識していますが、 Google Apps Script Editor の補完の動きを見ていると変数に代入されたオブジェクトの型を追っているようなので、これくらいできてくれてもいいんじゃないか、って思いました。

というか、普段全く IDE を触らず、辛うじて ER Master を使うためだけに Eclips を使ったりする程度なので、 IDE ってこんなものなのか、、、って残念な気持ちになりました。 余談ですが、 Titanium でアプリを開発するときも Vim でコードを書いて、コンパイルもターミナルからコンパイラを叩いていました。

タスクリマインダー

勉強会開催までには幾つものタスクがあるのですが、 CombConf 運営チームはなかなかタスクに手を付けられずに終わってるべきタスクが未着手とかいう事態が発生します。 そこでぼくが意識してタスクをバシバシ前に進める推進力になろうと意識しているのですが、ぼくも怠け者なのでタスクを発火させる役割の人間がタスクを発火していないというだめだこりゃ状態が発生する可能性があります。

そこで、わくわくエンジニアとして、タスクのリマインドを自動化しようと考えました。 そのタスクリマインダーに、触りたくてウズウズしていた Google Apps Script を組み合わせられないか考えました。 そうしたら、スタッフを務めていた PyCon JP 2012 では Google Spreadsheet と Google Apps Script を使って毎朝タスクリマインダーが送られてきていたことを思い出しました。

これだ、ということで、 Google Spreadsheet と Google Apps Script を使ったタスクリマインダーをCombConf にも導入することにしました!

書いた!

var USERS_SHEET_NAME = 'users',
    TASKS_SHEET_NAME = 'tasks',
    SPREADSHEET_URI = 'スプレッドシートのURI';

var getEmailByNickname = (function(){
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var usersSheet = spreadsheet.getSheetByName(USERS_SHEET_NAME);
  var userEmailTable = {};

  for(var i = 2; i <= usersSheet.getLastRow(); ++i){
    userEmailTable[usersSheet.getRange(i, 1).getValue()] = usersSheet.getRange(i, 2).getValue();
  }

  return function (nickname){
    return userEmailTable[nickname];
  };
})();

var getAllUserNicknames = function(){
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var usersSheet = spreadsheet.getSheetByName(USERS_SHEET_NAME);

  var users = [];
  for(var i = 2; i <= usersSheet.getLastRow(); ++i){
    var nickname = usersSheet.getRange(i, 1).getValue();
    if((nickname in users) === false){
      users.push(nickname);
    }
  }

  return users;
};

var getTasks = function(){
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var tasksSheet = spreadsheet.getSheetByName(TASKS_SHEET_NAME);
  var tasks = {};

  for(var i = 2; i <= tasksSheet.getLastRow(); ++i){
    var task = {
      name: tasksSheet.getRange(i, 1).getValue(),
      limit: new Date(Date.parse(tasksSheet.getRange(i, 2).getValue())),
      charge: tasksSheet.getRange(i, 3).getValue(),
      progress: tasksSheet.getRange(i, 4).getValue()
    };

    if(task.progress >= 100){
      continue;
    }

    if(task.charge in tasks){
      tasks[task.charge].push(task);
    }else{
      tasks[task.charge] = [task];
    }
  }

  return tasks;
};

var getTaskState = function(task) {  // 1:超過, 2:今日まで, 3:進行中
    var today = new Date();
    today.setHours(0);
    today.setMinutes(0);
    today.setSeconds(0);
    today.setMilliseconds(0);

    if(task.limit.getTime() < today.getTime()){
      return 1;
    }else if(task.limit.getTime() === today.getTime()){
      return 2;
    }else{
      return 3;
    }
};


var getFormatedDate = function(date){
  return date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate();
};


var getFormatedTask = function(task, embed_nickname){
  if(embed_nickname === true){
    return task.name + ' [担当:' + task.charge + '][期日:' + getFormatedDate(task.limit) + '][進捗:' + task.progress + '%]\n';
  }else{
    return task.name + ' [期日:' + getFormatedDate(task.limit) + '][進捗:' + task.progress + '%]\n';
  }
};


function sendTaskReminderMail2Inviduas(){
  var tasks = getTasks();
  for(var charge in tasks){
    var overdue = '',
        today = '',
        fight = '';

    for(var i = 0; i < tasks[charge].length; ++i){
      var task = tasks[charge][i];
      if(!task){
        continue;
      }
      switch(getTaskState(task)){
        case 1:
          overdue += getFormatedTask(task);
          break;
        case 2:
          today += getFormatedTask(task);
          break;
        case 3:
          fight += getFormatedTask(task);
          break;
      }
    }

    var body = 'タスクリマインダーメール\n'
             + 'タスクの追加や進捗状況の変更は' + SPREADSHEET_URI + 'から行なってください。\n'
             + '\n==========期限切れ==========\n'
             + overdue
             + '\n==========今日まで==========\n'
             + today
             + '\n==========頑張って==========\n'
             + fight;

    sendEmail(
      getEmailByNickname(charge),
      'タスクリマインダー for ' + charge,
      body
    );
  }
};


function sendTaskReminderMail2All(){
  var tasks = getTasks(),
      overdue = '',
      today = '',
      fight = '';

  for(var charge in tasks){
    for(var i = 0; i < tasks[charge].length; ++i){
      var task = tasks[charge][i];
      if(!task){
        continue;
      }
      switch(getTaskState(task)){
        case 1:
          overdue += getFormatedTask(task, true);
          break;
        case 2:
          today += getFormatedTask(task, true);
          break;
        case 3:
          fight += getFormatedTask(task, true);
          break;
      }
    }
  }

  var body = 'タスクリマインダーメール\n'
           + 'タスクの追加や進捗状況の変更は' + SPREADSHEET_URI + 'から行なってください。\n'
           + '\n==========期限切れ==========\n'
           + overdue
           + '\n==========今日まで==========\n'
           + today
           + '\n==========頑張って==========\n'
           + fight;

  var nicknames = getAllUserNicknames();
  for(var i = 0; i < nicknames.length; ++i){
    sendEmail(
      getEmailByNickname(nicknames[i]),
      'タスクリマインダー',
      body
    );
  }
};


var sendEmail = function(recipient, title, body){
  MailApp.sendEmail(recipient, '[自動送信メール]' + title, body);
};

使い方

  1. 新しいスプレッドシートを作る
  2. シートを追加する
    • タスクを書いていくためのシートと、通知先のメールアドレスを書いていくためのシート、計2枚のシートが必要です
  3. わかりやすいようにシートの名前を適当に変更する(オプション)
  4. ツール -> スクリプトエディタ -> スプレッドシート
  5. 上記のコードをペースト
  6. タスクを書くシートの名前をTASKS_SHEET_NAME に設定する
  7. ユーザーを書くシートの名前をUSERS_SHEET_NAME に設定する
  8. タスク用のシートに以下のフォーマットでタスクを書く
    • 1列目: タスク名
    • 2列目: 期日(YYYY/MM/DD)
    • 3列目: 担当者
    • 4列目: 進捗度(百分率)
  9. 通知先メールアドレス用のシートに以下のフォーマットでニックネームとメールアドレスのペアを書いていく
    • 1列目: ニックネーム
      • タスク用シートの担当者名と1:1 で対応する必要があります
    • 2列目: 通知先メールアドレス

※各シートの1行目は項目名として使われることを想定しているので、1行目に書いてあるタスクやメールアドレスは読まれません。

ありがとうございました!

寝てないのでさくっと書くつもりでしたが、気づいたら2610文字の長文になってました。 お読み頂きありがとうございました。 Happy Hacking!!!