Notionのデータベースをかみさんと共有する

Notionのデータベースをかみさんと共有する

最近は、スケジュール管理から、ちょっとしたメモをする際は、「Notion」というアプリを活用しています。
このアプリのすごいところは、データをデータベースで管理することができるところだと思います。詳しくは別の項で解説しておりますので、併せてご覧ください。
私は、自分のNotion内に、自身の仕事やプライベートの用事も含めたスケジュール管理とタスク管理を一つのデータベースで管理しています。

最近は、仕事以外でも会議や研修会などの用事が多く、かみさんとのスケジュール調整が大きな課題となっております。
私もかみさんも用事を入れてしまうと、家が子供だけになってしまいます。子どももそこそこ大きくはなりましたが、一応それでもお互いの都合を調整して、どちらかが家にいるようにしたほうがいいと考えています。

そんなとき、相手のスケジュールがいつでも確認できれば便利ですよね。

Notionでは、ページ単位で他のユーザーと情報を共有する機能があり、私のスケジュールカレンダーにかみさんを招待することで、私のスケジュールを閲覧できるようになります。

ただ、それには一つ問題があります。

私のスケジュールカレンダーには、私用以外に、仕事のスケジュールやタスクが一緒くたに入っています。
そのため、そのままそれをかみさんに共有してしまうと、仕事上の情報までも閲覧できてしまうことになってしまいます。あくまで個人が作成しているスケジュールなので、会社の機密事項などを入れることはありませんが、かみさんにとっても見たくない情報でしょう。

というわけで、その解決策を探ります。

注記:
最終的な解決策をお求めの方は、このページの一番下にpythonのサンプルコードがありますので、そちらをご覧ください。
お時間があるマルタスフリークの皆さまは、ぜひともゆるりとふじまるの追体験をしていただければと思います。( ´艸`)

まず、下図のように、私のスケジュール&タスク情報を2種類に切り分けます。

かみさんと共有したいプライベートのスケジュール&タスク情報(以下、「プライベート用DB」)だけをかみさんに見せるようにすればいいわけです。

データベースを2個用意して手動コピー

最初やったのは、プライベート用DBと共有用DBの2つを用意して、自分のスケジュールを登録後、必要に応じて、共有用DBにスケジュールをコピーする手法です。

先に「情報を2つに切り分ける」と話しましたが、特にデータにマークを付けるというわけでもなく、ただ登録時に「これは共有した方がいいな」と思ったら、同じアイテムを共有用DBにもコピーして作るというものです。

お分かりかと思いますが、それはそれは面倒でした。苦笑

最近Notionは、スケジュール(アイテム)を複製すると、「タイトル (1)」みたいに、コピーしたアイテムに、連番が付くような仕様になりまして。
ちょっとカッコ悪いと思っているので、共有用DBにコピーするたびに、タイトルの「(1)」といった部分を削る作業をする羽目になってしまいました。面倒で仕方ありません。

こりゃたまらんので、まもなくして別の方法を模索しました。

フィルタリングしたリンクドビューを共有する

ChatGPTが提案してきたのは、かみさんと共有するページを作って、そこにプライベート情報だけにフィルターをかけたカレンダーを表示するというものです。

なんだ、Notionには最初からそんな機能があるんじゃん!

今回からデータベースに「共有」というチェックボックスタイプのプロパティを追加しました。
共有したいと思ったアイテムの共有プロパティをチェックすることによって、共有したいアイテムのみを抽出したカレンダーなどが作成できます。

上記のように、「共有カレンダー」というページを作成し、そこに、共有=Trueという条件のスケジュールのみを抽出したカレンダービューの「リンクドビュー」を設置します。

Notionでの「ビュー」というのは、データベースの「見せ方」みたいなもので、アイテムをカレンダー表示したり、一定の条件のアイテムのみを一覧表示したりと、様々な条件や見た目で表示形式を設定することができます。

さらに「リンクドビュー」というのは、Notionの機能のひとつで、元のビューのURLを段落に張り付けることで、ページの好きな場所にビューを表示することができる機能です。

ビューを開く際は、そのデータベースを開いて、次にビューを選択して、、と表示させますが、いちいちそんな操作をしなくても、自分がよく使うビューをリンクドビューで並べたページを作っておけば、操作コストが抑えられますね。

ChatGPTは、「リンクドビューであれば、元のDBのアクセス権がなくてもページの閲覧権限さえあれば大丈夫!」と話していましたが、誤りでした。

これを実現するには、結局かみさんが元のデータベースへのアクセス権限を持たないと、かみさんはリンクドビューにアクセスできませんでした。

結局かみさんには、どれだけ抽出したリンクドビューに案内しようが、すんなりと直接DBにアクセスでき、すべてのスケジュール&タスク情報を閲覧できることになります。これではこだわる意味がありません。

外部自動化ツールを使う(Zapier or Make)

こうなったら、よく聞く「外部自動化ツール」を使ってみようと思います。

ChatGPTがいうには、ZapierやMakeといった自動化ツールによって、Notionを外部から操作して、最初にやっていたアイテムのコピーを実現しようとのことです。

個人的にあまりいろんなサイトにユーザー登録するのが好きではないため、できればNotion内だけで完結したかったのですが、ここまで来たら引き下がれません。

Zapierに登録します。

Zapierは、ノーコードでブロックを繋げていくことで、Notionのデータベースアイテムをコピーしたりする作業を行うことができます。便利な世の中です。

最初にNotionの「コネクト」という機能で、外部からNotionのDBを操作できるように「インテグレーション」というものを作成します。カタカナだらけで何が何やら。

要は、Notionのデータの操作権限をZapierと共有するようなもので、生成された秘密のシークレットトークンというキーワードを言えば、部屋の中に手を突っ込んでもいいよってな感じです。わかる?
てか「秘密」の「シークレット」トークンだなんて、「頭痛が痛い」と言ってるようなもんですね。表現が重複してます。

しかし、単純に元DBで更新されたアイテム(ページ)をZapierでガンガンコピーしても、ちょっとでも内容を変更するたびに、共有DB側に同じアイテムがどんどん増殖してしまいます。

つまり、更新されたアイテムが、コピー先DBに既にあるものなのか、新規のものかを判断しないといけません。
そのため、コピー先のアイテムには、コピー元のアイテムの一意となるキーを持たせ、元DBの更新されたアイテムのキーを持つアイテムを、共有DB側がすでに持っていたらそのアイテムは更新、なければ新規追加、という流れを作らないといけません。

そのため、まず共有用DBに、「親ID」というプロパティを作成します。Notionのアイテムには「ページID」という、ページ毎に一意の(ほかにダブりがない)数値があり、それをコピー先が保持することで、新規か更新かを見分けることができます。

次に下図のようなステップを作って繋げていきます。

私は新規登録用のZap(作成した自動化の流れの単位)と更新用のZapの2つのタスクを作成しました。

お~Zapierってデバッグ機能も充実していて、とても使いやすいですね。

ここで皆さんに私が作成したZapをお見せしたかったのですが、お見せすることができず申し訳ございません。
というのも実は後で知ったのですが、Zapierはフリープランではステップを2つまでしか作成できないという制限があるのです。

私はユーザーを登録した直後だったので、有料プランのトライアル期間が勝手に効いていたため、上記のような多くのステップを作ってつなぎ合わせることができていました。

しかし数日後、Zapierから「Zapを止めたぞ」といったメールが届き、確認してみると、2ステップ以上のZapを実行するには有料プランに加入しなければならないというのでした。

私のように「共有」プロパティを確認するような判断を入れただけで、Zapのフリープランは利用できないということになります。最初に言って〜!

ChatGPTにその旨を伝えると、「ではMakeを使ってみて!」と言われたので、さっそくユーザー登録をしたのですが、こちらも2ステップまででした(フリープラン)

あと、生成AIと付き合うには、ちゃんと別で確認したほうがいいです。
無駄にユーザー登録してしまいました。

とにかくお金をかけないで生活したいふじまるにとって、これらの選択肢は消滅してしまったのでした。

pythonで構築

もうあきらめかけていた私に、ChatGPTが最後の提案をしてきました。

チャッピー
pythonで自動化できるよ。

コードを書いてみようか?

結局、自分で1から構築するってわけです。

pythonかぁ・・・結構前に勉強していましたが、今では書き方のほとんどが頭に残っていません。

しかも最近PCを買い替えた私にとって、pythonの開発環境も構築していない状況。しかもpythonで動かせたとして、それを定期的に実行するサーバー環境が必要ではありませんか。

・・・いや、そういえばサーバー環境は24時間絶賛営業中のRaspberry Piがあるな、、、。こいつで動かすようにすれば実現できるのではないか?

こうなったらやってのけてしまおう!

一念発起、こうなったらやり尽くそう!
python、Visual Studio Codeをインストールし、python環境を整えたのでした。

今回行いたい処理の内容は大体こんな感じですが、、、

ふじまる
・・・。

最近のコーディング界隈は、イノベーションが起こっておりました。

ChatGPTがコードを生成してくれることは以前から使ったことはありました。いやこれだけでも十分にイノベーションなのですが、今回何よりも驚いたのが、Visual Studio Codeで動くコード補完機能です。

そりゃ、世の中には私のようにpythonでNotionをコントロールしようという人は多くいるとは思いますが、私の行動の先を予測してくるんです。

まず私がこれから行う処理のコメント行を書き出したら、コメントを補完してきます。

例えば、「# 親DBから更新された」と書いただけで続けて「データを取得」という日本語を補完してきます。私のやることがわかるの?

しかも、そのコメントを採用しようとtabキーを押すと、次の瞬間、親DBから更新されたデータを取得するpythonコードが自動生成されました。

pythonの書き方すらままならない私の先を行く補完機能のすごさに、私は置いてけぼりを食らってしまいました。

pythonの書き方を思い出しながら、自動生成されたコードを眺めてみると、私の書きたかったコードがそこにすでに書かれてありました。

いやこれは驚くべきイノベーションではありませんか。

私がちょっとだけVisual Studio Codeの世界から離れていましたら、世界はこんなに変わっていたのですね。まさに浦島太郎状態でした。

というわけで、ChatGPTとVisual Studio CodeのCopilot(なのかな?)に助けてもらいながら、あっという間にPythonコードは完成。Raspberry Piにpythonコードを転送し、cron(定期的にジョブを実行するタスクスケジューラー)を設定して実行してみたら、見事自動的にデータをシンクロさせることに成功しました。

サンプルコードはこちらです。

from notion_client import Client
from datetime import datetime, timezone, timedelta
import os

# Integrationで発行されたシークレットトークン
notion = Client(auth="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")

# データベースID(URLの?以降を除いた部分)
# 親DB(個人用スケジュール&タスク)
parentDatabase_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 子DB(共有用スケジュール&タスク)
childDatabase_id  = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# 保存ファイル名
SYNC_FILE = "last_sync_time.txt"

# 前回同期日時を取得
if os.path.exists(SYNC_FILE):
    with open(SYNC_FILE, "r") as f:
        last_sync_time = f.read().strip()
else:
    # ファイルが存在しない場合は、1週間前を設定
    last_sync_time = (datetime.now(timezone.utc) - timedelta(weeks=1)).isoformat()

# 現在日時を取得(最後に保存)
current_time = datetime.now(timezone.utc)

# 親DBから更新されたデータを取得(共有=trueのデータのみ)
parentDB_items = []
next_cursor = None

# ページネーション対応(100件ずつしか取得できないため、繰り返し取得)
while True:
    response = notion.databases.query(
        **{
            "database_id": parentDatabase_id,
            "filter": {
                "and": [
                    {
                        "property": "共有",
                        "checkbox": {
                            "equals": True
                        }
                    },
                    {
                        "timestamp": "last_edited_time",
                        "last_edited_time": {
                            "on_or_after": last_sync_time
                        }
                    }
                ]
            },
            "start_cursor": next_cursor
        }
    )
    parentDB_items.extend(response['results'])
    next_cursor = response.get('next_cursor')
    if not next_cursor:
        break

# 子DBに同期するデータを取得
for item in parentDB_items:
    parent_id = item['id']

    childDB_items = notion.databases.query(
        **{
            "database_id": childDatabase_id,
            "filter": {
                "property": "親ID",
                "rich_text": {
                    "equals": parent_id
                }
            }
        }
    )['results']

    if childDB_items:
        # 子DBに既に存在する場合は更新
        child_id = childDB_items[0]['id']
        notion.pages.update(
            **{
                "page_id": child_id,
                "icon": item['icon'],
                "properties": {
                    # 更新する項目をここに列挙
                    "タスク名": item['properties']['タスク名'],
                    "日付": item['properties']['日付'],
                    "タグ": item['properties']['タグ'],
                    "共有": item['properties']['共有'],
                }
            }
        )

    else:
        # 子DBに存在しない場合は新規作成
        notion.pages.create(
            **{
                "parent": { "database_id": childDatabase_id }, # parentは作成するDBのIDを指定
                "icon": item['icon'],
                "properties": {
                    # コピーする項目をここに列挙
                    "タスク名": item['properties']['タスク名'],
                    "日付": item['properties']['日付'],
                    "タグ": item['properties']['タグ'],
                    "共有": item['properties']['共有'],
                    "親ID": {
                        "rich_text": [
                            {
                                "type": "text",
                                "text": {
                                    "content": parent_id
                                }
                            }
                        ]
                    }
                }
            }
        )

# 現在日時を保存
with open(SYNC_FILE, "w") as f:
    f.write(current_time.isoformat())

このコードが約1日で完成しました。AIが助けてくれなかったら、数日は要していたと思います。あまりにも予想で出てくるコードがその通り過ぎて、若干怖さを感じました。

生成されたコードを読み解いて、「なるほど~」と唸った自分が面白かったです。

というわけで今回は終わります。

生成AIとは上手に付き合え、とか言われたりしますが、こうしたコーディング周りのサポートに関してはべったり頼ってしまってもいいような気がします。Microsoft officeのVBAなども自動的にコードを補完してくれたらずいぶん楽だろうなと思います。今のOffice365のCopilotにはそんな機能があるのかもしれませんが、Office365のサブスクリプションには加入しておりませんのでわかりません。とにかくお金をかけたくないので。

私は自作ゲームを作ることを夢見て毎日パソコンと向き合っておりますが、こうしたAIの進歩によって、その作業が捗ればいいなぁと思います。なかなか作り始めることができませんが。

それではまたノシ