Googleカレンダー連携タイムラインを作る【GAS】
以前、Googleスプレッドシートのタスクを元にjQueryライブラリにてタイムラインアプリを作成したことがあります。同じ仕組みを作って、Google Groupに参加してるメンバーのカレンダー情報を投影できないか?ということで今回作成してみました。
外部メンバーでもGoogleアカウントであるならば表示させることは可能ですが、相手に閲覧を許可しておく必要性があります。
所謂、組織カレンダーと呼ばれるものを模して作成したもので、同様のものはrakumoやサテライトオフィスがアドオンとして提供している。元々はCybozeのスケジュールカレンダーが発祥と思われるこの機能日本企業だと結構需要があるのです。
目次
今回利用するスプレッドシート
- 組織カレンダーアプリ - Google Spreadsheet
- タイムラインライブラリ
- Google Visualization API
- Admin SDK
利用する為には事前準備が必要です。また、今回はグループのメンバーデータを取得する為にAdmin SDKを使って管理コンソールからデータを取得する仕様になっているので、管理者権限を持つものでウェブアプリをデプロイする必要があります。
また今回利用してるライブラリがGoogle Visualization APIのDataTableを利用してる為、そのライブラリも追加されています。
※fullCalendarだとより高度がことが可能ですがタイムライン機能はPremiumの機能なのでライセンス購入が必要です($480くらい)。
図:タイムライン表示のメイン画面
利用にあたっての事前準備
GAS側
コード内
GASのコード内に於いて、コード.gsの上部にあるdomain変数に自身のテナントのドメインを入力します。Admin SDKを利用してグループのリストやそのメンバー情報を取得する為に必要です。
図:ドメインを入力して保存
HTML内の参照ライブラリ
今回のサンプルの場合、index.htmlがウェブアプリの本体になります。このファイル冒頭のHead部分でタイムラインライブラリ等を参照していますが、JSやCSSの一部がこのサーバに格納してるライブラリを指定しています。ここはご自身で前述のタイムラインライブラリをダウンロードして、参照するURLを書き直しておくと良いでしょう。
図:サンプルはこのサーバ上のライブラリを使ってます
設定を行う
スプレッドシートのメニューに表示されてる「タイムライン」を開くと2つ項目が出てきます。それぞれを実行します。
- セットアップ:スプレッドシートのスクリプトプロパティにファイルのIDが記述されます(ウェブアプリ用)
- グループ書き出し:Google Workspaceのテナントに作成済みのグループアドレス一覧をシートに書き出します(これがカレンダー表示のソースになる)
ただし、グループについては組織カレンダーに合うものだけに削ったほうが良いでしょう。手動で登録しても問題ありません。また、アプリケーションの仕様として
- グループアドレスの中にグループアドレスがあっても無視されます(ユーザのみが対象となるし子グループ内まで探索しません)
- グループアドレス内にあるメンバーはGoogleアカウントである必要があります(M365ユーザが居てもカレンダーデータは取得出来ません)
- 外部メンバーであってもGoogleアカウントであるならばカレンダーデータは取得可能です。
- カレンダーデータはタイトルと時間のみを取得し、ユーザ名とメアドのみがAdmin SDKで参照されます。
- 今回のアプリではユーザに直接紐付くデフォルトカレンダーのみが対象。別途作ったカレンダーについては検証していません。
- デプロイした人のカレンダーには、ユーザがカレンダーにアクセスする毎に個人のカレンダーが自動subscribeされて追加されていきます。
図:セットアップが必要
Webアプリケーションとしてデプロイ
事前準備が完了したら、ウェブアプリケーションとしてデプロイします。デプロイしたものを社内ポータルのGoogle Sitesなどに貼り付けると良いでしょう。
- スクリプトエディタを開く
- 右上のデプロイをクリック
- 新しいデプロイをクリック
- 種類の選択ではウェブアプリを選択し、次のユーザとしてアプリケーションを実行で誰の権限で動かすかを指定する。今回は自分自身を指定します。
- アプリケーションにアクセスできるユーザを指定する。今回は組織内ユーザに限定します。
- 最後に導入すると、ウェブアプリケーションのURLが取得できます。このURLでアクセスをします。URLの最後がexecが本番用、devがテスト用で、テスト用はデプロイをテストをクリックすると表示されますが、変更したコードがそのまますぐに反映されてしまうので、テスト用のURLで運用しないように。
- 次回以降コードを編集して再デプロイ時はデプロイを管理から同じURLにて、新しいバージョンを指定して発行することが出来ます。
図:今回は次のユーザとして実行は管理者権限のあるアカウントにて
一般ユーザ側
Googleカレンダーは仕様上、相手が自分自身と共有していて尚且つ自身のカレンダーに相手のカレンダーを追加登録していないとデータが取得出来ません。よって、ユーザサイドでは以下の設定をしておく必要があります。
- Googleカレンダーを開く
- マイカレンダー一番上にある自分自身に直接紐付くカレンダーにカーソルを合わせて、「︙」をクリック
- 設定と共有をクリックする
- 予定のアクセス権限にて、xxxxで利用できるようにするにチェックを入れて、内容は時間枠のみ・詳細は非表示だとタイトルが取れないので、すべての予定の詳細を選択する
- もしくは、下のほうにある「特定のユーザーまたはグループと共有する」にて、ユーザやグループを追加をクリック。
- デプロイした人のアドレスを入れて共有する
図:設定と共有を開く
図:デプロイした人にカレンダーが見えないと取得出来ない
ソースコード
GAS側コード
//グループアドレス一覧を取得する //全グループの情報を取得する function getAllGroups(){ let admin = GetUser(); let ui = SpreadsheetApp.getUi(); if(admin == "master@officeforest.org"){ //そのまま処理を続行する }else{ ui.alert("管理者権限でなければ実行できません"); return; } //API用変数 let pageToken; let array = []; do{ //アドレスリストを取得する try{ var groupList = AdminDirectory.Groups.list( { domain: domain, maxResults: 200, pageToken: pageToken } ); //グループ名とアドレスを取得する for(var i = 0;i<groupList.groups.length;i++){ //グループを取得する let group = groupList.groups[i]; //グループ情報を取得する let mail = group.email; let name = group.name; //配列に追加する let temparr = [ mail, name ] array.push(temparr) } }catch(e){ console.log(e.message) } //次のページトークンを取得する pageToken = groupList.nextPageToken; }while(pageToken) //スプレッドシート取得 let ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("groups") //スプシをクリアする ss.getRange("A2:B").clearContent(); //一括書き出し let lastColumn = array[0].length; //カラムの数を取得する let lastRow = array.length; //行の数を取得する ss.getRange(2,1,lastRow,lastColumn).setValues(array); //終了メッセージ ui.alert("書き出しが完了しました。") } //グループリストを取得して返す function getGroups(){ //シートIDを取得する let prop = PropertiesService.getScriptProperties(); let ssid = prop.getProperty("sheetid"); //スプシのデータを取得する let ss = SpreadsheetApp.openById(ssid).getSheetByName("groups").getRange("A2:B").getValues(); //スプシデータを返す return JSON.stringify(ss); } //指定グループメンバーのカレンダーデータを取得する(デフォルトは現時点から1週間分だけ) function recalendar(groupman,flg, startman, endman){ //グループのメンバーリストを取得する let members = getGroupsMembers(groupman); //ユーザのカレンダーデータを取得する let caldata = []; for(let i = 0;i<members.length;i++){ //アドレスを一個取り出す let user = members[i]; //ユーザカレンダー情報を取得する getaddcalendar(user[0]) let usrCalendar = CalendarApp.getCalendarById(user[0]); //カレンダー日付を取得する //フラグで処理を分岐 let startDate; let endDate; if(flg == 0){ //グループ選択時の設定 startDate = new Date(); //開始日付 endDate = new Date(); endDate.setDate(startDate.getDate() + 8); //1週間後の日付を指定(8の理由は7日目のデータが7では取れない為1日足しておく) }else{ //期間を指定して再取得時の設定 startDate = new Date(startman); endDate = new Date(endman); endDate.setDate(startDate.getDate() + 1); //1日加算しておかないとendDate当日のデータが取れない } //指定期間のイベントを取得 let usrEvent; usrEvent = usrCalendar.getEvents(startDate, endDate); //イベント登録が全く無い場合にダミーで登録 if(usrEvent.length == 0){ caldata.push([formatCaldate(new Date(),0),formatCaldate(new Date(),0),"",user[1]]) continue; } //イベントデータを格納する for(let i = 0; i< usrEvent.length; i++){ //レコードを取得する let rec = usrEvent[i]; //一時配列を用意する let temparr = []; //イベントデータを格納する temparr.push(formatCaldate(rec.getStartTime(),0)); //開始日付 temparr.push(formatCaldate(rec.getEndTime(),0)); //終了日付 temparr.push(rec.getTitle()); //イベントタイトル temparr.push(user[1]); //ユーザ名 //barrayに追加する caldata.push(temparr); } } //データを返す return JSON.stringify(caldata); } //日付を加工する関数 function formatCaldate(datetime,flg){ //日付を取得 let dt = new Date(datetime); //日付を取得 let yearman = dt.getFullYear(); let monthman = paddingZero(dt.getMonth() + 1); let dateman = paddingZero(dt.getDate()); //時間を取得 let hourman = paddingZero(dt.getHours()); let mins = paddingZero(dt.getMinutes()); //整形して返す if(flg == 0){ var strDate = yearman + "/" + monthman + "/" + dateman + " " + hourman + ":" + mins; }else{ var strDate = String(yearman) + String(monthman) + String(dateman) + String(hourman) + String(mins) } return strDate; } //グループメンバーを取得する function getGroupsMembers(groupman){ //所属メンバーを調べる group = GroupsApp.getGroupByEmail(groupman); let members = getMemberlist(group); return members; } //グループのメンバー情報を取得する本体関数 function getMemberlist(group){ //配列を用意する let member = []; //グループメンバーを取得する let users = group.getUsers(); //直属メンバー情報を取得する for (let i = 0; i < users.length; i++) { //ユーザを取得する let user = users[i]; //アドレスと氏名を取得する let address = user.getEmail() let username = ""; //外部ユーザはアドレスから、内部ユーザはディレクトリから氏名を取得 try{ username = AdminDirectory.Users.get(address) username = username.name.fullName }catch(e){ username = user.getUsername(); } //配列に追加 let temparr = []; temparr.push(address) temparr.push(username) //返却用配列に追加 member.push(temparr) } //2次元配列を返す return member; } //頭に0をつける var paddingZero = function(n) { return (n < 10) ? '0' + n : n; }; //他のユーザのカレンダーを他のカレンダーとして追加する function getaddcalendar(mail){ var calendar = CalendarApp.subscribeToCalendar(mail); return 0 } //現在のユーザのアドレスを取得 function GetUser() { var objUser = Session.getActiveUser(); return objUser.getEmail(); }
- 殆どのコードが指定のグループアドレスからグループ情報の取得、メンバーの取得、メンバーの詳細情報を取得する関数になります。
- 個別カレンダーを取得する為にsubscribeToCalendarにて自動でカレンダー追加を行わせています。
- recalendar関数がメインの関数。デフォルトは1週間分のデータを取得します。
- 日付期間を指定してカレンダーデータを取得する場合の条件分岐をrecalendar関数で指定。その際に最終日+1日しないと最終日のカレンダーデータが取れないので加工しています。
- 指定期間内のカレンダーにイベントがないと、その人が組織カレンダー一覧に出てこなくなってしまうのでダミーで空のデータを必ず入れています。
カレンダーデータの取得周りは以下のエントリーで別にまとめています。
HTML側コード
<head> 省略 <style type="text/css"> 省略 </style> <script type="text/javascript"> var timeline; var groups = []; var timeheight = 500; var calevent = []; //リサイズ時にウィンドウにフィットさせる window.onresize = function(){ setGridHeight(); } //gridのサイズを自動でウィンドウにフィットする $(document).ready(function () { setGridHeight(); }); //Visualization APIの呼び出し google.charts.load('current', {'packages': ['corechart'],'language': 'ja'}); google.charts.setOnLoadCallback(initialize); //描画エリア自動リサイズ function setGridHeight() { //高さの自動算出 var layoutHeight = $(window).height() - 65; //エレメント操作 $('.timeline-frame').css('height', layoutHeight); $('#mytimeline').css('height', layoutHeight + 'px'); //高さの指定数値を変更 timeheight = layoutHeight //描画し直し timeline.redraw(); console.log("OK") } //GAS側から対象シートのデータを取得する function initialize() { //グループ一覧を取得する google.script.run.withSuccessHandler(onGroups).getGroups(); } //グループデータを取得する function onGroups(data){ //データを取得する let json = JSON.parse(data); //1つ目のグループアドレスを取得する let groupman = json[0]; //取得データに基づいてselect optionを構築する let html = "<option value='notselect'>グループを選択</option>" for(let i = 0;i<json.length;i++){ //レコードを一個取り出す let rec = json[i]; //空レコードはスルーする if(rec[0] == "" || rec[0] == undefined){ continue; } //オプションを構築する html = html + "<option value='" + rec[0] + "'>" + rec[1] + "</option>" } //wasabi4に書き出し document.getElementById("wasabi4").innerHTML = html } //シートデータを反映する function onSuccess(data){ //データを取得する var json = JSON.parse(data); //変数にも格納する calevent = JSON.parse(data); //データテーブルを構築する var datatable = new google.visualization.DataTable(); datatable.addColumn('date', '開始日'); datatable.addColumn('date', '終了日'); datatable.addColumn('string', 'タイトル'); datatable.addColumn('string', '担当者'); //データテーブルに取得データを追加する var array = []; for(var i = 0;i<json.length;i++){ //一時配列を用意する let temparray = []; //一時配列にpushする temparray.push(new Date(json[i][0])); temparray.push(new Date(json[i][1])); temparray.push(json[i][2]); temparray.push(json[i][3]); //arrayに追加する array.push(temparray); } //datatableにarrayを追加する datatable.addRows(array); // オプションの指定 var options = { width: "95%", height: timeheight, minHeight: 400, layout: "box", groupsOnRight: false, groupsChangeable : false, eventMargin: 10, eventMarginAxis: 0, showNavigation: true, axisOnTop: true, locale:"ja", showCurrentTime:true, }; //timelineを表示する timeline = new links.Timeline(document.getElementById('mytimeline'), options); //イベントリスナーを追加する google.visualization.events.addListener(timeline, 'select', onselect); timeline.draw(datatable); timeline.redraw(); //現在時刻へジャンプ timeline.setVisibleChartRangeNow(); //再描画 setGridHeight(); } //タイムライン選択時のイベント var onselect = function (event) { var row = getSelectedRow(); if (row != undefined) { //イベントデータを表示する alert(calevent[row][2]) timeline.unselectItem(row); }else{ } }; //指定した期間にスケール表示する為のルーチン function setTime() { //エラートラップ処理 if (!timeline){ return; } //指定の日付を取得する var startdate = document.getElementById("startDate").value; var enddate = document.getElementById("endDate").value; //指定グループを取得する var group = document.getElementById("wasabi4").value; //GAS側に指定のアドレスでデータを再取得 google.script.run.withSuccessHandler(onSuccess).recalendar(group,1,startdate,enddate); } //項目をクリックした時のrow番号を取得して返す関数 function getSelectedRow() { var row = undefined; var sel = timeline.getSelection(); if (sel.length) { if (sel[0].row != undefined) { row = sel[0].row; } } return row; } //アクセスコントロール function a_ctrl(){ //選択名をシート名に代入する group = document.getElementById("wasabi4").value; if(group == "グループを選択"){ return } //データテーブルを初期化する google.script.run.withSuccessHandler(onSuccess).recalendar(group,0); } </script> </head> <body style="font-family: Arial;border: 0 none;"> <p> <b>開始日:</b> <input type="text" id="startDate" class="type1" value="" style="width: 100px;"> <b>終了日:</b> <input type="text" id="endDate" class="type1" value="" style="width: 100px;"> <input type="button" class="create" id="setStartDate" value="指定期間を表示" onclick="setTime();"> <b>グループ:</b> <select name='wasabi4' id='wasabi4' onChange='a_ctrl()' title='プロジェクトを選択'></select> </p> <script> //日付ピッカーの表示用スクリプト $(".type1").datepicker({ showWeek: true, firstDay: 1, }); </script> <div id="mytimeline"></div> </body>
- 基本的には前回の記事の内容とほぼ同じ
- ただし、カレンダーデータの取得をするため、選択中グループアドレスの取得などに変更をしています。
- またウィンドウをリサイズした際にフィットするようにsetGridHeightなどの関数をイベントに追加しています。
- 省略したCSS部分で個別のパーツのカスタマイズを追加しています。
- 予定クリック時に予定のタイトルを表示するように変更しました。
ベースになるアプリであるため、まだ色々と足りない部分は多いと思います(例えばこの上から予定を追加するであったり、予定クリック時の詳細な内容を表するロジックなど)。また、デプロイした人の管理者権限で動いてる為、個別の予定の追加となるとデプロイした人のカレンダーに予約が書き込まれてしまうので、以下のエントリーのようなサービスアカウントとGoogle APIを利用して登録する仕組みが別途必要です。