0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンターのバックグラウンドサーバスクリプトでGoogleカレンダーと期限付きテーブルを双方向同期する方法

0
Posted at

はじめに

Googleカレンダーで管理しているスケジュールをプリザンターの期限付きテーブルに取り込んだり、逆にプリザンターで登録したタスクをGoogleカレンダーに反映したいケースがあります。手動で二重入力するのは非効率で、入力漏れや不整合の原因にもなります。

今回は、バックグラウンドサーバスクリプトを使って、Googleカレンダーとプリザンターの期限付きテーブルを定期的に双方向同期する仕組みを作ってみます。

記事中のAPIリクエスト例は Api.json の Compatibility_1_3_12 が false(既定値)であることを前提としています。APIバージョンの詳細については「既定のAPIバージョン 1.1 への変更および旧バージョンとの互換性について」を参照してください。

処理の全体像

バックグラウンドサーバスクリプトがスケジュール実行されると、まずGoogleカレンダーからイベントを取得してプリザンターにUpsertし(①)、次にプリザンター側のレコードをGoogleカレンダーに反映します(②)。GoogleのイベントIDを分類項目に格納することで、同期済みレコードの識別と重複防止を実現します。

前提条件

Script.json

バックグラウンドサーバスクリプトを有効にするために、App_Data/Parameters/Script.jsonのBackgroundServerScriptをtrueに設定します。

App_Data/Parameters/Script.json
{
    "ServerScript": true,
    "BackgroundServerScript": true
}

パラメータファイルの変更後はプリザンターの再起動が必要です。パラメータ再読み込み機能を使う場合は、特権ユーザでログインして実行してください。

APIキー

プリザンターのレコード取得・更新にはAPIキーが必要です。対象テーブルの編集権限を持つユーザのAPIキーを作成しておきます。

期限付きテーブルの作成

Googleカレンダーのイベントを格納する期限付きテーブルを作成します。以下のカラム構成にします。

カラム 用途 説明
タイトル イベント名 Googleカレンダーのイベントタイトルを格納
内容 イベント詳細 イベントの説明を格納
開始 開始日時 イベントの開始日時を格納
完了 終了日時 イベントの終了日時を格納
分類A GoogleイベントID GoogleカレンダーのイベントIDを格納。同期のキーとして使用
分類B 同期元 google または pleasanter を格納。レコードの作成元を識別

カラムの種類は環境に合わせて変更してください。ここでは分類A・分類Bを使用していますが、分類C以降でも構いません。スクリプト内のフィールド名(ClassA・ClassB)を変更する場合は、対応する箇所をすべて修正してください。

テーブルを作成したら、サイトIDを控えておきます。後述のスクリプトに設定します。

Google Calendar APIの準備

Google Cloudプロジェクトの設定

  1. Google Cloud Consoleにアクセスし、プロジェクトを作成または選択する
  2. 「APIとサービス」→「ライブラリ」から Google Calendar API を検索して有効化する
  3. 「APIとサービス」→「認証情報」→「認証情報を作成」→「OAuthクライアントID」を選択する
  4. アプリケーションの種類は「デスクトップアプリ」を選択して作成する
  5. クライアントIDとクライアントシークレットを控えておく

リフレッシュトークンの取得

プリザンターのサーバスクリプトはJavaScript(V8 / ClearScript)エンジンで動作しますが、OAuthの対話的認証フローは実行できません。そのため、事前にリフレッシュトークンを取得しておき、スクリプトではリフレッシュトークンからアクセストークンを取得する方式を採用します。

OAuth 2.0 Playgroundを使ってリフレッシュトークンを取得します。

  1. OAuth 2.0 Playgroundにアクセスする
  2. 右上の歯車アイコンをクリックし、「Use your own OAuth credentials」にチェックを入れる
  3. 控えておいたクライアントIDとクライアントシークレットを入力する
  4. 左側の「Select & authorize APIs」で https://www.googleapis.com/auth/calendar を選択して「Authorize APIs」をクリックする
  5. Googleアカウントでログインし、アクセスを許可する
  6. 「Exchange authorization code for tokens」をクリックする
  7. 表示されたリフレッシュトークンを控えておく

リフレッシュトークンはGoogleカレンダーへのフルアクセス権限を持つ認証情報です。安全な場所に保管し、ソースコード管理システムにはコミットしないでください。

カレンダーIDの確認

同期対象のGoogleカレンダーのIDを確認します。

  1. Googleカレンダーを開く
  2. 対象カレンダーの「設定と共有」を開く
  3. 「カレンダーの統合」セクションにあるカレンダーIDを控える

プライマリカレンダーの場合、カレンダーIDはGmailアドレスと同じです。

アクセストークン取得関数

リフレッシュトークンを使ってアクセストークンを取得する関数を用意します。アクセストークンには有効期限があるため、スクリプト実行のたびに新しいトークンを取得します。

/**
 * リフレッシュトークンからアクセストークンを取得する
 */
function getAccessToken(clientId, clientSecret, refreshToken) {
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri = 'https://oauth2.googleapis.com/token';
    httpClient.Content =
        'client_id=' + encodeURIComponent(clientId)
        + '&client_secret=' + encodeURIComponent(clientSecret)
        + '&refresh_token=' + encodeURIComponent(refreshToken)
        + '&grant_type=refresh_token';
    httpClient.MediaType = 'application/x-www-form-urlencoded';
    var response = httpClient.Post();

    if (!httpClient.IsSuccess) {
        context.Log('アクセストークン取得に失敗しました');
        return null;
    }

    var result = JSON.parse(response);
    return result.access_token;
}

httpClient.MediaTypeをapplication/x-www-form-urlencodedに設定し、トークンエンドポイントにPOSTリクエストを送信します。レスポンスのJSON内のaccess_tokenを返します。

httpClient.ResponseHeaders.Clear() はリクエストの前に必ず呼び出してください。詳しくは「プリザンターのサーバスクリプトでhttpClientを使うときのお約束」を参照してください。

Googleカレンダー → プリザンター

Googleカレンダーのイベントを取得し、プリザンターの期限付きテーブルにUpsertします。

/**
 * Googleカレンダーのイベントをプリザンターに取り込む
 */
function syncGoogleToPleasanter(accessToken, calendarId, targetSiteId) {
    // 同期対象期間(過去30日〜未来90日)
    var now = new Date();
    var timeMin = new Date(now);
    timeMin.setDate(timeMin.getDate() - 30);
    var timeMax = new Date(now);
    timeMax.setDate(timeMax.getDate() + 90);

    var url = 'https://www.googleapis.com/calendar/v3/calendars/'
        + encodeURIComponent(calendarId) + '/events'
        + '?timeMin=' + timeMin.toISOString()
        + '&timeMax=' + timeMax.toISOString()
        + '&singleEvents=true'
        + '&orderBy=startTime'
        + '&maxResults=250';

    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri = url;
    httpClient.RequestHeaders['Authorization'] = 'Bearer ' + accessToken;
    var response = httpClient.Get();

    if (!httpClient.IsSuccess) {
        context.Log('Googleカレンダーのイベント取得に失敗しました');
        return 0;
    }

    var result = JSON.parse(response);
    var events = result.items || [];
    var upsertCount = 0;

    events.forEach(function (event) {
        try {
            if (event.status === 'cancelled') return;

            var startTime = event.start.dateTime || event.start.date;
            var endTime = event.end.dateTime || event.end.date;

            var data = {
                Keys: ['ClassA'],
                Title: event.summary || '(無題)',
                Body: event.description || '',
                ClassA: event.id,
                ClassB: 'google',
                StartTime: startTime,
                CompletionTime: endTime
            };

            var upsertResult = items.Upsert(targetSiteId, JSON.stringify(data));

            if (upsertResult) {
                upsertCount++;
            }
        } catch (e) {
            context.Log('イベント取り込みエラー: ' + event.id + ' - ' + e.message);
        }
    });

    return upsertCount;
}

処理の解説

処理 説明
同期対象期間 timeMin(過去30日)からtimeMax(未来90日)のイベントを取得。期間は環境に合わせて調整
singleEvents=true 繰り返しイベントを個別の単発イベントとして展開する
maxResults=250 1回のリクエストで取得する最大件数。Google Calendar APIの上限は2500件
Keys: ['ClassA'] GoogleのイベントID(分類A)をUpsertのキーに指定。同じイベントIDのレコードは更新、新しいイベントは作成
event.start.dateTime 時刻指定イベントの場合はISO 8601形式の日時。終日イベントの場合はevent.start.dateに日付のみが入る
event.status cancelledのイベントはスキップ

繰り返しイベント(毎週の定例会議など)はsingleEvents=trueを指定すると個別のイベントとして取得されます。各イベントには繰り返しの日時に応じた固有のIDが付与されるため、Upsertで正しく管理されます。

プリザンター → Googleカレンダー

プリザンターの期限付きテーブルのレコードをGoogleカレンダーに反映します。分類AにGoogleのイベントIDがないレコードは新規作成、あるレコードは更新します。

/**
 * プリザンターのレコードをGoogleカレンダーに反映する
 */
function syncPleasanterToGoogle(
    accessToken, calendarId, targetSiteId, apiKey, baseUrl
) {
    // プリザンターから全レコードを取得
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri = baseUrl + 'api/items/' + targetSiteId + '/get';
    httpClient.Content = JSON.stringify({
        ApiVersion: 1.1,
        ApiKey: apiKey,
        View: {
            ApiGetPageSize: 500
        }
    });
    httpClient.MediaType = 'application/json';
    var response = httpClient.Post();

    if (!httpClient.IsSuccess) {
        context.Log('プリザンターのレコード取得に失敗しました');
        return { created: 0, updated: 0 };
    }

    var result = JSON.parse(response);
    var records = result.Response.Data || [];
    var created = 0;
    var updated = 0;

    records.forEach(function (record) {
        try {
            var googleEventId = record.ClassA;
            var title = record.Title;
            var body = record.Body || '';
            var startTime = record.StartTime;
            var completionTime = record.CompletionTime;

            // 開始日時が未設定のレコードはスキップ
            if (!startTime) return;

            // 終了日時が未設定の場合は開始日時の1時間後を設定
            if (!completionTime) {
                var end = new Date(startTime);
                end.setHours(end.getHours() + 1);
                completionTime = end.toISOString();
            }

            var eventData = {
                summary: title,
                description: body,
                start: { dateTime: startTime },
                end: { dateTime: completionTime }
            };

            if (googleEventId) {
                // 既存イベントの更新
                updateGoogleEvent(
                    accessToken, calendarId, googleEventId, eventData
                );
                updated++;
            } else {
                // 新規イベントの作成
                var newEventId = createGoogleEvent(
                    accessToken, calendarId, eventData
                );
                if (newEventId) {
                    // 作成されたイベントIDをプリザンターに保存
                    saveEventId(
                        record.IssueId, newEventId, apiKey, baseUrl
                    );
                    created++;
                }
            }
        } catch (e) {
            context.Log(
                'レコード同期エラー: ' + record.IssueId + ' - ' + e.message
            );
        }
    });

    return { created: created, updated: updated };
}

Googleカレンダーへのイベント作成

/**
 * Googleカレンダーにイベントを作成し、イベントIDを返す
 */
function createGoogleEvent(accessToken, calendarId, eventData) {
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri =
        'https://www.googleapis.com/calendar/v3/calendars/'
        + encodeURIComponent(calendarId) + '/events';
    httpClient.Content = JSON.stringify(eventData);
    httpClient.MediaType = 'application/json';
    httpClient.RequestHeaders['Authorization'] = 'Bearer ' + accessToken;
    var response = httpClient.Post();

    if (!httpClient.IsSuccess) {
        context.Log('Googleイベント作成に失敗しました');
        return null;
    }

    var created = JSON.parse(response);
    return created.id;
}

Googleカレンダーのイベント更新

/**
 * Googleカレンダーのイベントを更新する
 */
function updateGoogleEvent(accessToken, calendarId, eventId, eventData) {
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri =
        'https://www.googleapis.com/calendar/v3/calendars/'
        + encodeURIComponent(calendarId) + '/events/'
        + encodeURIComponent(eventId);
    httpClient.Content = JSON.stringify(eventData);
    httpClient.MediaType = 'application/json';
    httpClient.RequestHeaders['Authorization'] = 'Bearer ' + accessToken;
    httpClient.Patch();

    if (!httpClient.IsSuccess) {
        context.Log('Googleイベント更新に失敗しました: ' + eventId);
    }
}

イベントIDの保存

Googleカレンダーにイベントを作成した後、返されたイベントIDをプリザンターのレコードに保存します。

/**
 * プリザンターのレコードにGoogleイベントIDを保存する
 */
function saveEventId(recordId, googleEventId, apiKey, baseUrl) {
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri =
        baseUrl + 'api/items/' + recordId + '/update';
    httpClient.Content = JSON.stringify({
        ApiVersion: 1.1,
        ApiKey: apiKey,
        ClassA: googleEventId
    });
    httpClient.MediaType = 'application/json';
    httpClient.Post();

    if (!httpClient.IsSuccess) {
        context.Log('イベントID保存に失敗しました: ' + recordId);
    }
}

バックグラウンドサーバスクリプト

登録手順

  1. 特権ユーザでログイン
  2. 「テナント管理」→「サーバスクリプト」タブを開く
  3. 「新規作成」をクリック
  4. 以下を設定して「追加」→「更新」
項目 設定値
タイトル Googleカレンダー双方向同期
条件 バックグラウンドサーバスクリプト
スケジュール 毎時(任意)
無効 チェックなし
サーバスクリプト 下記参照

スクリプト

// --- 設定 ---
var apiKey = 'YOUR_API_KEY';
var baseUrl = context.ApplicationPath;
var targetSiteId = 12345;                      // 期限付きテーブルのサイトID
var calendarId = 'example@gmail.com';          // GoogleカレンダーID
var clientId = 'YOUR_CLIENT_ID';               // OAuth クライアントID
var clientSecret = 'YOUR_CLIENT_SECRET';       // OAuth クライアントシークレット
var refreshToken = 'YOUR_REFRESH_TOKEN';       // OAuth リフレッシュトークン
// --- 設定ここまで ---

// アクセストークンを取得
var accessToken = getAccessToken(clientId, clientSecret, refreshToken);
if (!accessToken) return;

// ① Googleカレンダー → プリザンター
var importCount = syncGoogleToPleasanter(
    accessToken, calendarId, targetSiteId
);
context.Log('Google → Pleasanter: ' + importCount + '件');

// ② プリザンター → Googleカレンダー
var exportResult = syncPleasanterToGoogle(
    accessToken, calendarId, targetSiteId, apiKey, baseUrl
);
context.Log(
    'Pleasanter → Google: 作成=' + exportResult.created
    + '件, 更新=' + exportResult.updated + '件'
);

// --- 関数定義 ---

function getAccessToken(clientId, clientSecret, refreshToken) {
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri = 'https://oauth2.googleapis.com/token';
    httpClient.Content =
        'client_id=' + encodeURIComponent(clientId)
        + '&client_secret=' + encodeURIComponent(clientSecret)
        + '&refresh_token=' + encodeURIComponent(refreshToken)
        + '&grant_type=refresh_token';
    httpClient.MediaType = 'application/x-www-form-urlencoded';
    var response = httpClient.Post();

    if (!httpClient.IsSuccess) {
        context.Log('アクセストークン取得に失敗しました');
        return null;
    }

    return JSON.parse(response).access_token;
}

function syncGoogleToPleasanter(accessToken, calendarId, targetSiteId) {
    var now = new Date();
    var timeMin = new Date(now);
    timeMin.setDate(timeMin.getDate() - 30);
    var timeMax = new Date(now);
    timeMax.setDate(timeMax.getDate() + 90);

    var url = 'https://www.googleapis.com/calendar/v3/calendars/'
        + encodeURIComponent(calendarId) + '/events'
        + '?timeMin=' + timeMin.toISOString()
        + '&timeMax=' + timeMax.toISOString()
        + '&singleEvents=true'
        + '&orderBy=startTime'
        + '&maxResults=250';

    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri = url;
    httpClient.RequestHeaders['Authorization'] = 'Bearer ' + accessToken;
    var response = httpClient.Get();

    if (!httpClient.IsSuccess) {
        context.Log('Googleカレンダーのイベント取得に失敗しました');
        return 0;
    }

    var result = JSON.parse(response);
    var events = result.items || [];
    var upsertCount = 0;

    events.forEach(function (event) {
        try {
            if (event.status === 'cancelled') return;

            var startTime = event.start.dateTime || event.start.date;
            var endTime = event.end.dateTime || event.end.date;

            var data = {
                Keys: ['ClassA'],
                Title: event.summary || '(無題)',
                Body: event.description || '',
                ClassA: event.id,
                ClassB: 'google',
                StartTime: startTime,
                CompletionTime: endTime
            };

            var upsertResult = items.Upsert(
                targetSiteId, JSON.stringify(data)
            );

            if (upsertResult) {
                upsertCount++;
            }
        } catch (e) {
            context.Log(
                'イベント取り込みエラー: ' + event.id + ' - ' + e.message
            );
        }
    });

    return upsertCount;
}

function syncPleasanterToGoogle(
    accessToken, calendarId, targetSiteId, apiKey, baseUrl
) {
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri = baseUrl + 'api/items/' + targetSiteId + '/get';
    httpClient.Content = JSON.stringify({
        ApiVersion: 1.1,
        ApiKey: apiKey,
        View: {
            ApiGetPageSize: 500
        }
    });
    httpClient.MediaType = 'application/json';
    var response = httpClient.Post();

    if (!httpClient.IsSuccess) {
        context.Log('プリザンターのレコード取得に失敗しました');
        return { created: 0, updated: 0 };
    }

    var result = JSON.parse(response);
    var records = result.Response.Data || [];
    var created = 0;
    var updated = 0;

    records.forEach(function (record) {
        try {
            var googleEventId = record.ClassA;
            var title = record.Title;
            var body = record.Body || '';
            var startTime = record.StartTime;
            var completionTime = record.CompletionTime;

            if (!startTime) return;

            if (!completionTime) {
                var end = new Date(startTime);
                end.setHours(end.getHours() + 1);
                completionTime = end.toISOString();
            }

            var eventData = {
                summary: title,
                description: body,
                start: { dateTime: startTime },
                end: { dateTime: completionTime }
            };

            if (googleEventId) {
                updateGoogleEvent(
                    accessToken, calendarId, googleEventId, eventData
                );
                updated++;
            } else {
                var newEventId = createGoogleEvent(
                    accessToken, calendarId, eventData
                );
                if (newEventId) {
                    saveEventId(record.IssueId, newEventId, apiKey, baseUrl);
                    created++;
                }
            }
        } catch (e) {
            context.Log(
                'レコード同期エラー: ' + record.IssueId + ' - ' + e.message
            );
        }
    });

    return { created: created, updated: updated };
}

function createGoogleEvent(accessToken, calendarId, eventData) {
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri =
        'https://www.googleapis.com/calendar/v3/calendars/'
        + encodeURIComponent(calendarId) + '/events';
    httpClient.Content = JSON.stringify(eventData);
    httpClient.MediaType = 'application/json';
    httpClient.RequestHeaders['Authorization'] = 'Bearer ' + accessToken;
    var response = httpClient.Post();

    if (!httpClient.IsSuccess) {
        context.Log('Googleイベント作成に失敗しました');
        return null;
    }

    return JSON.parse(response).id;
}

function updateGoogleEvent(accessToken, calendarId, eventId, eventData) {
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri =
        'https://www.googleapis.com/calendar/v3/calendars/'
        + encodeURIComponent(calendarId) + '/events/'
        + encodeURIComponent(eventId);
    httpClient.Content = JSON.stringify(eventData);
    httpClient.MediaType = 'application/json';
    httpClient.RequestHeaders['Authorization'] = 'Bearer ' + accessToken;
    httpClient.Patch();

    if (!httpClient.IsSuccess) {
        context.Log('Googleイベント更新に失敗しました: ' + eventId);
    }
}

function saveEventId(recordId, googleEventId, apiKey, baseUrl) {
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri = baseUrl + 'api/items/' + recordId + '/update';
    httpClient.Content = JSON.stringify({
        ApiVersion: 1.1,
        ApiKey: apiKey,
        ClassA: googleEventId
    });
    httpClient.MediaType = 'application/json';
    httpClient.Post();

    if (!httpClient.IsSuccess) {
        context.Log('イベントID保存に失敗しました: ' + recordId);
    }
}

スクリプトの解説

スクリプト先頭の設定セクションで接続情報を指定します。

設定項目 説明
apiKey プリザンターのAPIキー
baseUrl プリザンターのベースURL。context.ApplicationPathを使用
targetSiteId 期限付きテーブルのサイトID
calendarId GoogleカレンダーのID(Gmailアドレスまたはカレンダー固有のID)
clientId Google OAuth2.0のクライアントID
clientSecret Google OAuth2.0のクライアントシークレット
refreshToken Google OAuth2.0のリフレッシュトークン

処理の流れは以下のとおりです。

  1. getAccessTokenでリフレッシュトークンからアクセストークンを取得する
  2. syncGoogleToPleasanterでGoogleカレンダーのイベントをプリザンターに取り込む
    • singleEvents=trueで繰り返しイベントを展開して取得する
    • items.UpsertでイベントID(分類A)をキーにUpsertする
  3. syncPleasanterToGoogleでプリザンターのレコードをGoogleカレンダーに反映する
    • 分類AにイベントIDがないレコードはcreateGoogleEventで新規作成する
    • 作成されたイベントIDをsaveEventIdでレコードに保存する
    • 分類AにイベントIDがあるレコードはupdateGoogleEventで更新する
  4. 処理結果をcontext.Logで出力する

context.ApplicationPathはプリザンターのルートパスを返します。サブディレクトリ配置(例: https://example.com/pleasanter/)の場合は /pleasanter/ が返されるため、API URLが正しく組み立てられます。

同期の競合について

双方向同期では、同じイベントがGoogleカレンダーとプリザンターの両方で同時に変更された場合に競合が発生する可能性があります。このスクリプトでは以下のルールで処理します。

状況 動作
Googleで変更、プリザンターで未変更 ① でGoogleの内容がプリザンターに反映される
プリザンターで変更、Googleで未変更 ② でプリザンターの内容がGoogleに反映される
両方で変更 ① でGoogleの内容がプリザンターに反映された後、② で上書きされたプリザンター側の内容がGoogleに再反映される(結果的にGoogleの内容が優先)

このスクリプトではGoogleカレンダー側の変更が優先されます。プリザンター側の変更を優先したい場合は、② → ① の順序に変更してください。更新日時による厳密な競合解決が必要な場合は、updatedMinパラメータやレコードのUpdatedTimeを使った差分同期の実装を検討してください。

カスタマイズ例

終日イベントへの対応

Google Calendar APIでは、終日イベントの場合はdateTimeではなくdateフィールドが使用されます。プリザンター → Googleカレンダーの同期で終日イベントを作成したい場合は、以下のようにeventDataの構築を変更します。

var eventData;
var startDate = new Date(startTime);
var endDate = new Date(completionTime);

// 開始と終了が同じ日の場合は終日イベントとして扱う
if (startDate.toDateString() === endDate.toDateString()) {
    // 終日イベント(date形式)
    var nextDay = new Date(startDate);
    nextDay.setDate(nextDay.getDate() + 1);
    eventData = {
        summary: title,
        description: body,
        start: { date: startDate.toISOString().split('T')[0] },
        end: { date: nextDay.toISOString().split('T')[0] }
    };
} else {
    // 時刻指定イベント(dateTime形式)
    eventData = {
        summary: title,
        description: body,
        start: { dateTime: startTime },
        end: { dateTime: completionTime }
    };
}

Google Calendar APIの終日イベントでは、終了日は「その日を含まない」仕様です。たとえば3月10日の終日イベントを作成する場合、start.dateは2026-03-10、end.dateは2026-03-11を指定します。

特定ステータスのレコードのみ同期

プリザンター側で特定のステータスのレコードだけをGoogleカレンダーに同期したい場合は、API取得時にColumnFilterHashでフィルタリングできます。

httpClient.Content = JSON.stringify({
    ApiVersion: 1.1,
    ApiKey: apiKey,
    View: {
        ColumnFilterHash: {
            Status: '[100,150,200]'
        },
        ApiGetPageSize: 500
    }
});

この例では、ステータスが100(未着手)、150(準備)、200(実行中)のレコードだけを同期対象にしています。完了したタスクをGoogleカレンダーに反映しない運用ができます。

Googleカレンダーで削除されたイベントの処理

Googleカレンダー側でイベントが削除された場合、APIレスポンスのstatusがcancelledになります。この情報を使って、プリザンター側のレコードのステータスを自動的に更新できます。

// syncGoogleToPleasanter 内の forEach に追加
if (event.status === 'cancelled') {
    // 削除されたイベントに対応するレコードのステータスを更新
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri = baseUrl + 'api/items/' + targetSiteId + '/get';
    httpClient.Content = JSON.stringify({
        ApiVersion: 1.1,
        ApiKey: apiKey,
        View: {
            ColumnFilterHash: {
                ClassA: event.id
            }
        }
    });
    httpClient.MediaType = 'application/json';
    var findResponse = httpClient.Post();

    if (httpClient.IsSuccess) {
        var findResult = JSON.parse(findResponse);
        var matched = findResult.Response.Data || [];
        matched.forEach(function (rec) {
            httpClient.ResponseHeaders.Clear();
            httpClient.RequestUri =
                baseUrl + 'api/items/' + rec.IssueId + '/update';
            httpClient.Content = JSON.stringify({
                ApiVersion: 1.1,
                ApiKey: apiKey,
                Status: 900
            });
            httpClient.MediaType = 'application/json';
            httpClient.Post();
        });
    }
    return;
}

この例では、Googleカレンダーで削除されたイベントに対応するレコードのステータスを900(完了)に変更しています。

削除イベントの取得にはshowDeleted=trueパラメータをイベント一覧取得URLに追加する必要があります。

まとめ

バックグラウンドサーバスクリプトとGoogle Calendar APIを組み合わせて、プリザンターの期限付きテーブルとGoogleカレンダーの双方向同期を実現しました。

  • バックグラウンドサーバスクリプトでスケジュール実行を自動化した
  • OAuth2.0のリフレッシュトークンを使って、事前認証なしでアクセストークンを取得する仕組みにした
  • items.UpsertでGoogleのイベントIDをキーにして重複なくレコードを登録・更新した
  • プリザンターで作成したレコードをGoogleカレンダーに新規イベントとして登録し、返されたイベントIDを保存して以降の同期に使用した
  • 既存イベントはPatchメソッドで差分更新した
  • context.ApplicationPathを使用しサブディレクトリ配置にも対応した
  • Script.jsonのBackgroundServerScript: trueが前提条件
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?