🗂

NotionとSwiftBarによるタスク管理のすゝめ

2024/06/27に公開

はじめに

この記事では、タスク管理が苦手な自分が実施したタスク管理についてまとめています。
タスク管理が苦手な方の何かお役に立てる記事になると嬉しいです。
今回、コードの綺麗さなどは担保しておりません。ご了承くださいませ。

想定読者

  • Macユーザー
  • Notionでタスク管理を行なっている方&これから行いたい方
  • Notion APIを使って何かしてみたい方
  • タスク管理に悩んでいる方

タスク管理での悩み

皆さんは、どのようにタスク管理を行なっていますか。

GoogleスプレッドシートやExcel、Notion、その他タスク管理ツールなど、様々なツールを活用していると思います。今回の記事を読んでいる方の中には、頭の中でタスク管理を完結させている猛者もいるかもしれません。
しかし、そんなタスク管理ですが、多くの人が何かしら悩みを抱えているのではないでしょうか。

私の場合、特に「タスクの追加忘れ」と「見直す習慣がない」の2点が非常に大きな問題でした。
過去、五月雨で依頼されるタスクを頭の中でどうにか管理しようとしており、漏れたタスク達を慌てて対応していた時期も多々ありました。

(特に突発的な依頼から発生する)タスクの追加を忘れてしまう

Slackや会議、雑談やメールなど様々なツール経由でいろんなタスクが振ってきます。
数分でできるものであれば、なるべくすぐやってしまうのですが、優先度やタスクの重さ的に後回しにしちゃうこともよくあります。
そして、Notion(タスク管理ツール)を開いてなく、タスク追加も後回しにしちゃうということが頻発してしまいました。

タスクを見直す習慣がない

そもそも1日のはじめにタスクを見直して、終業時に確認&更新して終業するというフローを実現できてませんでした。1人でやる業務が多い私はなかなかタスク管理を強制的・組織的に行うことができていませんでした。そのような状況に甘え、頭の中でタスク管理をやり続けていました。

また、「タスク管理をやるぞ!!!」と意気込んでタスク管理ツールを準備をしても、完璧にやり切りたいという気持ちが湧いてしまい、なかなか続かないことがしばしばありました。

私でもできるタスク管理を考えてみた

悩みを解決するために実現すべきこと

  • Notionを開かなくてもNotionでタスク管理ができること。
    • 追加・編集・削除など...。
  • すぐにタスクを追加することが可能であること。

妥協したこと

  • 綺麗に入れることはやめる(頭の中でタスク管理をしないことを最重視する)。
  • 入力のしやすさを完璧に求めない。

こだわりたいこと

  • ユーザーが設定したプロパティ名で実行できること。
    • メインコードの変更をしなくて済むことを目指します。
  • メニューバーからタスクの表示、追加、編集、削除ができること。
  • GUIでタスクの追加・編集が実施できること。
  • 期限が過ぎているかつチェックがついていないタスクにチェックをつけることができる機能。
  • メニューバーからNotionデータベースにアクセス可能であること。
  • チェックがついていないタスクも表示できること。

解決策

タスク管理を行う上で「管理したい時に、Notionを開いてない」という点が管理ができない一番の理由だと感じたため、「どのツールを開いていても、メニューバーに表示可能である」SwiftBarを活用したタスク管理を実施してみました。

アウトプットイメージ
アウトプットイメージ

必要なツール

  • Notion
    • タスク管理用のデータベースを作成するため。
  • SwiftBar
    • メニューバーからタスクにアクセスするため。
  • Zenity
    • タスク追加/編集時のGUIとして利用するため。

設定

ここでは、「Notion」「SwiftBar」「Zenity」の設定を実施します。

Notionの設定

  • タスク管理用のNotionデータベースを作成
    • 参照:データベース作成方法

    • データベースのカラムを設定する(下表はデフォルト設定値 / 順不同)。
      ※プロパティの型は、APIを利用時に使用のため、ここでは気にせずOK

      プロパティ名 プロパティの種類 プロパティの型
      タスク名 タイトル title
      ToDo チェックボックス checkbox
      ステータス ステータス status
      期限 日付 date
      優先度 セレクト select
      メモ テキスト rich_text

Notionデータベースの例
タスク管理用Notionデータベースのイメージ

参考:Notionデータベースのテンプレート

  • インテグレーションを作成しAPIトークンを取得する

  • NotionDBのIDを取得する

    • 作成したNotionDBを開く
      • 開いたURLの32桁のDatabase_idを取得する
      • https://www.notion.so/{Database_id(32桁)}?v={View_id}
        • URL:https://www.notion.so/3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d?v=8e4b9797aa894041b8de89d4cf61ced4
        • Database_id : 3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d
        • View_id : 8e4b9797aa894041b8de89d4cf61ced4
  • NotionDBとインテグレーションをコネクトする

  • オートメーションの設定

    • ステータスが完了になると、チェックボックスを外す処理を追加
    • プラン次第では、オートメーションの設定ができないため、その際は、別途Pythonスクリプトを実装する必要があります。(本記事では対象外とします)

SwiftBarの設定

  • SwiftBarのGithubリポジトリはこちら

  • SwiftBarのインストール

  • プラグインディレクトリの設定(後から変更可能)

    • 詳細はこちら
      ※後から作成するPythonファイルを設定する。

Zenityの設定

  • Zenityとは、簡単にダイアログを作成可能なもの。

ディレクトリ構成

notion-task-app
    ├── src
    │   ├── apps (※SwiftBarのプラグインディレクトリ)
    │   │   └── notion_task_management.10m.py
    │   └── script
    │       └── task_editor.sh
    ├── requirements.txt
    ├── column_setting.json
    ├── .env
    └── README.md

実際のソースコード

設定ファイル

環境変数の設定

上記で取得したAPIトークンとデータベースIDをこちらに設定します。

.env
##################
# 環境変数のサンプルファイル
# NOTION_API_TOKEN   : Notion APIのトークン
# DATABASE_ID        : 連携したいNotionデータベースのID
##################

NOTION_API_TOKEN = ''
DATABASE_ID      = ''
Notionのプロパティ名を設定

ここのJSONファイルのプロパティ名を取得し、メインコードに反映させています。
このことにより、プロパティ名の動的性を担保しています。

column_setting.json
{
    "title" : "タスク名",  
    "date" : "期限",
    "status" : "ステータス",
    "checkbox" : "ToDo",
    "select" : "優先度",
    "rich_text" : "メモ"
}

メインファイル

notion_task_management.10m.pyの全体

MENU_TITLEを変更することで、メニューバーに表示される文字を変更することができます。
※少し長いですが、ご了承くださいませ。

notion_task_management.10m.py
#!/usr/local/bin/python3

import requests
import json
import sys, os
from dotenv import load_dotenv
import subprocess
from datetime import datetime

# .envファイルを読み込む
load_dotenv()

# 環境変数からデータを取得
NOTION_API_TOKEN = os.getenv('NOTION_API_TOKEN')
DATABASE_ID = os.getenv('DATABASE_ID')

#ファイル名の設定
JSON_FILE_NAME = 'column_setting.json'
ZENITY_FILE_NAME = 'task_editor.sh'

## パスの取得
PYTHON_SCRIPT_PATH = os.path.abspath(__file__)
apps_dir = os.path.dirname(os.path.abspath(PYTHON_SCRIPT_PATH))
notion_task_app_dir = os.path.dirname(os.path.dirname(apps_dir))
ZENITY_SCRIPT_PATH = os.path.join(notion_task_app_dir, 'src', 'script', ZENITY_FILE_NAME)
JSON_PATH = os.path.join(notion_task_app_dir, JSON_FILE_NAME)

# メニューバーに表示するタイトル
MENU_TITLE = 'タスク一覧'

# Notion APIのエンドポイント
database_url = f"https://api.notion.com/v1/databases/{DATABASE_ID}/query"
page_url = "https://api.notion.com/v1/pages"
headers = {
    "Notion-Version" : "2022-06-28",
    "Authorization" : f"Bearer {NOTION_API_TOKEN}",
    "Content-Type" : "application/json"
}


# Jsonファイルの読み取り
with open(JSON_PATH, 'r') as file:
    notion_columns = json.load(file)

# 期限をISO 8601形式に変換する関数
def change_deadline(deadline):
    if deadline:
        try:
            deadline = datetime.strptime(deadline, "%Y/%m/%d").strftime("%Y-%m-%d")
            return deadline
        except ValueError:
            print("日付形式が正しくありません。")
            return None

# Zenityの起動する関数
def run_zenity(script_path): 
    command = [
        script_path, 
        notion_columns['title'],
        notion_columns['date'],
        notion_columns['rich_text']
        ]
    result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

    return result.stdout.strip().split("|")

# Notion DBからデータを取得する関数
def fetch_tasks(ch_box_bool):
    payload = {
        "filter": {
            "and": [
                {"property": notion_columns["status"],"status": {"does_not_equal": "完了"}},
                {"property": notion_columns["status"],"status": {"does_not_equal": "保留"}},
                {"property": notion_columns["checkbox"], "checkbox": {"equals": ch_box_bool}}
            ]
        },
        "sorts": [
            {"property": notion_columns["date"],"direction": "ascending"},  # 日付ソート
            {"property": notion_columns["select"],"direction": "ascending"} # 優先度ソート
        ]
    }

    response = requests.post(database_url, headers=headers, data=json.dumps(payload))

    if response.status_code == 401:
        print("認証エラー: APIトークンまたはデータベースのアクセス権限を確認してください。")
        print(response.text)
    else:
        return response.json()

# タスクを追加する関数
def add_task():
    task_name, deadline, memo = run_zenity(ZENITY_SCRIPT_PATH)

    if not task_name:
        print(f"{notion_columns['title']}は必須です。")
        return

    deadline = change_deadline(deadline)

    new_task = {
        "parent": {"database_id": DATABASE_ID},
        "properties": {
            notion_columns["title"]:{"title": [{"text": {"content": task_name}}]},
            notion_columns["checkbox"]:{"checkbox": True}
        }
    }

    if deadline:
        new_task["properties"][notion_columns["date"]] = {"date": {"start": deadline}}

    if memo:
        new_task["properties"][notion_columns["rich_text"]] = {"rich_text": [{"text": {"content": memo}}]}

    response = requests.post(page_url, headers=headers, data=json.dumps(new_task))
    check_response_status(response, "追加", "title")

# タスクを削除する関数
def delete_task(task_id):
    url = f"https://api.notion.com/v1/blocks/{task_id}"
    response = requests.delete(url, headers=headers)

    check_response_status(response, "削除", "title")

# タスクを編集する関数
def edit_task(task_id):
    url = f"https://api.notion.com/v1/pages/{task_id}"
    task_name, deadline, memo = run_zenity(ZENITY_SCRIPT_PATH)
    updated_task = {"properties": {}}
    deadline = change_deadline(deadline)

    if task_name:
        updated_task["properties"][notion_columns["title"]] = {"title": [{"text": {"content": task_name}}]}
    if deadline:
        updated_task["properties"][notion_columns["date"]] = {"date": {"start": deadline}}
    if memo:
        updated_task["properties"][notion_columns["rich_text"]] = {"rich_text": [{"text": {"content": memo}}]}

    response = requests.patch(url, headers=headers, data=json.dumps(updated_task))
    check_response_status(response, "更新", "title")

# チェックを外す関数
def uncheck_task(task_id):
    url = f"https://api.notion.com/v1/pages/{task_id}"

    updated_task = {"properties": {notion_columns["checkbox"]: {"checkbox": False}}}
    response = requests.patch(url, headers=headers, data=json.dumps(updated_task))
    
    check_response_status(response, "チェックを外す", "checkbox")

# ステータスを変更する関数
def change_status(task_id, new_status):
    updated_task = {"properties": {notion_columns["status"]: {"status": {"name": new_status}}}}

    url = f"https://api.notion.com/v1/pages/{task_id}"
    response = requests.patch(url, headers=headers, data=json.dumps(updated_task))

    check_response_status(response, "更新", "status")

# レスポンスのステータスを確認する関数
def check_response_status(response, action_name, property_name):
    if response.status_code == 200:
        print(f"{notion_columns[property_name]}が正常に{action_name}されました。")
    else:
        print(f"{notion_columns[property_name]}{action_name}に失敗しました。")
        print(response.text)


# 実行日より古い期限のタスクを取得する関数
def get_database_items(database_id):
    url = f"https://api.notion.com/v1/databases/{database_id}/query"
    # 今日の日付を取得
    today = datetime.now().date()
    payload = {
        "filter": {
            "and": [
                {"property": notion_columns["date"],"date": {"before": today.isoformat()}},
                {"property": notion_columns["status"],"status": {"does_not_equal": "保留"}},
                {"property": notion_columns["checkbox"], "checkbox": {"equals": False}}
            ]
        }
    }
    response = requests.post(url, headers=headers, json=payload)
    return response.json()['results']

# チェックボックスをTrueにする関数
def update_checkbox_property(task_id):
    url = f"https://api.notion.com/v1/pages/{task_id}"
    payload = {
        "properties": {notion_columns["checkbox"]: {"checkbox": True}}
    }

    response = requests.patch(url, headers=headers, json=payload)

    # データベースのアイテムを取得してチェックボックスを更新
    items = get_database_items(DATABASE_ID)
    for item in items:
        update_checkbox_property(item['id'])

def main():
    print(f":book.fill: {MENU_TITLE} | dropdown=true")
    print("---")
    print(f"{notion_columns['title']}を追加 | bash='{PYTHON_SCRIPT_PATH}' param2='add' terminal=false refresh=true")
    print(f"Notion DBを表示 | href=https://www.notion.so/{DATABASE_ID}")
    print(f"{notion_columns['title']}を更新 | refresh=true")
    print("---")
    task_chbox_true = fetch_tasks(True)
    task_chbox_false = fetch_tasks(False)

    if task_chbox_true:
        for task in task_chbox_true.get("results", []):

            task_name = task["properties"][notion_columns["title"]]["title"][0]["text"]["content"]
            task_id = task["id"]
            task_url = task["url"]

            # プロパティの存在を確認
            priority = ""
            if notion_columns["select"] in task["properties"] and task["properties"][notion_columns["select"]].get("select"):
                priority = task["properties"][notion_columns["select"]]["select"]["name"]

            status = ""
            if notion_columns["status"] in task["properties"] and task["properties"][notion_columns["status"]].get("status"):
                status = task["properties"][notion_columns["status"]]["status"]["name"]

            deadline = ""
            if notion_columns["date"] in task["properties"] and task["properties"][notion_columns["date"]].get("date"):
                deadline = task["properties"][notion_columns["date"]]["date"]["start"]

            memo = ""
            if notion_columns["rich_text"] in task["properties"] and task["properties"][notion_columns["rich_text"]].get("rich_text"):
                memo = task["properties"][notion_columns["rich_text"]]["rich_text"][0]["text"]["content"]
            
            print(f"{task_name} | href={task_url}")
            print(f"--{notion_columns['status']}を完了に変更 | bash='{PYTHON_SCRIPT_PATH}' param2='change_status' param3='{task_id}' param4='完了' terminal=false refresh=true")
            print(f"--編集 | bash='{PYTHON_SCRIPT_PATH}' param2='edit' param3='{task_id}' terminal=false refresh=true")
            print(f"--{notion_columns['select']} : {priority} | terminal=false")
            print(f"--{notion_columns['status']}: {status} | terminal=false")
            print(f"--{notion_columns['date']}: {deadline} | terminal=false")
            print(f"--{notion_columns['rich_text']}: {memo} | terminal=false")
            print(f"--{notion_columns['checkbox']}のチェックを外す | bash='{PYTHON_SCRIPT_PATH}' param2='uncheck_task' param3='{task_id}' terminal=false refresh=true")
            print(f"--{notion_columns['status']}を未着手に変更 | bash='{PYTHON_SCRIPT_PATH}' param2='change_status' param3='{task_id}' param4='未着手' terminal=false refresh=true")
            print(f"--{notion_columns['status']}を進行中に変更 | bash='{PYTHON_SCRIPT_PATH}' param2='change_status' param3='{task_id}' param4='進行中' terminal=false refresh=true")
            print(f"--削除 | bash='{PYTHON_SCRIPT_PATH}' param2='delete' param3='{task_id}' terminal=false refresh=true")

    if task_chbox_false:
        print("---")
        print(f"{notion_columns['checkbox']}にチェックなし{notion_columns['title']}一覧 | refresh=true")
        for task in task_chbox_false.get("results", []):
            task_name = task["properties"][notion_columns["title"]]["title"][0]["text"]["content"]
            task_id = task["id"]
            task_url = task["url"]
            print(f"--{task_name} | href={task_url}")

    print(f"チェックボックス初期化 | bash='{PYTHON_SCRIPT_PATH}' param2='update_ch_box' param3='{task_id}' terminal=false refresh=true")

if __name__ == "__main__":
    if len(sys.argv) > 1:
        command = sys.argv[1]
        if command == "add":
            add_task()
        elif command == "delete" and len(sys.argv) == 3:
            task_id = sys.argv[2]
            delete_task(task_id)
        elif command == "edit" and len(sys.argv) == 3:
            task_id = sys.argv[2]
            edit_task(task_id)
        elif command == "uncheck_task" and len(sys.argv) == 3:
            task_id = sys.argv[2]
            uncheck_task(task_id)
        elif command == "change_status" and len(sys.argv) == 4:
            task_id = sys.argv[2]
            new_status = sys.argv[3]
            change_status(task_id, new_status)
        elif command == "update_ch_box" and len(sys.argv) == 3:
            task_id = sys.argv[2]
            update_checkbox_property(task_id)
    else:
        main()

GUI(Zenity)ファイル

task_editor.shの全体
task_editor.sh
#!/bin/bash

# タスク名、期限、メモを取得するフォームの表示
form_result=$(
    zenity --forms \
    --title="新しい$1" \
    --text="$1の詳細を入力してください。" \
    --add-entry="$1" \
    --add-calendar="$2" \
    --add-entry="$3"
    )

# 入力結果をPythonスクリプトに渡す
echo "$form_result"

実際に使ってみた感想

初期のPoCも含め、1ヶ月以上このツールを運用・改善してきました。現在、タスクの追加や管理の漏れはなく、スムーズにタスクの追加と消化ができています。

また、今回は、タスクの追加だけでなく、ステータスの変更やToDoではないタスクも表示できるようにしました。
これにより、タスク全体の把握が可能となり、さらに期限を過ぎたがチェックがついていないタスクにチェックをつける機能を実装したことで、タスク消化漏れが減少したと感じています。

さらに、もっと便利にすべく日次・週次で行うタスクにはNotionデータベースの繰り返し処理を追加することで、タスク追加の手間を省くことも可能になりました。

最後に

普段コーディングしない私でも、ChatGPTを活用してなんとか形にすることができました。学習しやすい時代になったことに歓喜しております。

まだまだ改良の余地はあると思っており、次のステップとしては、下記2点を検討しています。

  • 各プロパティの選択肢(ステータスなら未着手・進行中・完了など)もユーザーが設定したものを柔軟にできるようにしたい
  • 入力の手間を軽減するために、音声でタスクを追加できる機能

ぜひ試してみた方は、コメントで感想をお聞かせください!

Aidemy Tech Blog

Discussion