NotionDBでタスク管理していると、毎回同じタスクのページを作るの面倒くさくないです?
さりとてテンプレート機能もイマイチしっくりこない、、、
ということで、Notion + GAS + Slackで、定期タスク管理とタスク発行ができる仕組みを作りました。
今回はインストール編です!
この仕組みでできること
-
NotionDBに、指定した間隔でページを追加できます。
- <対応している間隔>
- 日次、週次、月次、四半期次、半年次、年次
- 月次、四半期次、半年次、年次は、任意の基準日を設定してタスクを発行できます。
- <対応している間隔>
-
手順書のURLをドキュメントに記載できます。
- 本当は設定ページに書き込んだ内容をコピーさせたかったのですが、NotionAPIがクセ強めなので諦めました。
- 代わりに、プロパティに設定したURLを、ドキュメント本文に記入することで妥協しました。
-
土日祝でもタスクを発行するか選択できます。
- 平日ではない土日祝でも平日タスクを発行したい場合は、設定をONにすれば発行できます。
-
追加したページの通知をSlackに飛ばせます。
- Notionのオートメーション機能による通知もありますが、表示が冗長なので、GASでメッセージを整形して飛ばします。
ここでは説明しないこと
各サービスの操作方法及びAPIの利用開始方法
事前に準備するもの
- Notionアカウント(フリーでも可)
- GoogleアカウントもしくはGoogle Workspaceアカウント
- Slackアカウント(フリーでも可)
手順概要
- 各サービスで使用するAPI・アプリを設定
- 定期タスク設定用NotionDBを作成
- タスク管理DBを作成
- GASでコードとトリガーを設定
手順詳細
各サービスで使用するAPI・アプリを設定
必要な設定のみ記します。
- Notionインテグレーション
- スコープに下記を設定
- コンテンツを読み取る、コンテンツを挿入、メールアドレスなしでユーザー情報を読み取る
- スコープに下記を設定
- GoogleWorkspace
- 設定なし
- Slackアプリ
- 通知用チャンネルを指定したWebhookを取得する。
定期タスク設定用NotionDBを作成
定期タスクをどの間隔で作成するか指定するためのデータベースを作成します。
用意するプロパティ
プロパティ | 種類 | 説明 |
---|---|---|
発行する | チェックボックス | 発行したいタスクはONにします。 |
Validation | 数式 | 以降の項目で、項目「間隔」に応じて、各項目の入力バリデーションを行い、結果を表示します。 |
設定名 | タイトル | 発行したいタスクの名前を入力します。 |
間隔 | セレクト | タスクの発行間隔を選択します。 |
担当者 | ユーザー | タスクの担当者を選択します。 |
説明書URL | URL | タスクの説明書のURLを入力します。 |
日数 | 数値 | タスクの実施期間を日数で入力します。 |
発行基準日付 | 日付 | 項目「間隔」で、月次、四半期次、半年次、年次を選択した場合、入力した基準日に応じて発行対象日を計算し、処理当日の日付と同じだった場合にタスクを発行します。 |
発行基準曜日 | マルチセレクト | 項目「間隔」で週次を選択した場合、タスクを発行する曜日を選択し、処理当日の曜日と同じだった場合にタスクを発行します。 |
土日祝も発行する | チェックボックス | 項目「間隔」で日次を選択した場合、処理当日が土日祝でもタスクを発行するか指定します。 |
通知する | チェックボックス | タスクの発行結果をSlackに通知するか指定します。 |
各プロパティへの設定
- Validation:下記の数式を設定
Validationの数式
if(
prop("間隔") == "日次" and !empty(prop("設定名")) and !empty(prop("担当者")) and !empty(prop("説明書URL")) and prop("日数") >= 0,
"Valid",
if(
prop("間隔") == "週次" and !empty(prop("設定名")) and !empty(prop("担当者")) and !empty(prop("説明書URL")) and prop("日数") >= 0 and !empty(prop("発行基準曜日")),
"Valid",
if(
or(prop("間隔") == "月次",prop("間隔") == "四半期次",prop("間隔") == "半年次",prop("間隔") == "年次") and !empty(prop("設定名")) and !empty(prop("担当者")) and !empty(prop("説明書URL")) and prop("日数") >= 0 and !empty(prop("発行基準日付")),
"Valid",
style("InValid","b","yellow_background")
)
)
)
- 間隔:下記のオプションを設定
- 日次、週次、月次、四半期次、半年次、年次
- 担当者
- 制限:1ユーザー
- 日数
- 小数点以下の桁数0
- 発行基準日付
- 年月日、非表示
- 発行基準曜日:下記のオプションを設定
- 月、火、水、木、金、土、日
その他
データベースのコネクションに、NotionAPIのインテグレーションを登録してください。
タスク管理DBを作成
実際にタスク管理するためのデータベースを作成します。
用意するプロパティ
プロパティ | 種類 | 説明 |
---|---|---|
ステータス | ステータス | タスクの進捗を選択します。 |
タスク名 | タイトル | タスクの名前を入力します。 |
担当者名 | ユーザー | タスクの担当者を選択します。 |
開始日 | 日付 | タスクの開始日を入力します。 |
終了日 | 日付 | タスクの終了日を入力します。 |
その他
データベースのコネクションに、NotionAPIのインテグレーションを登録してください。
GASでコードとトリガーを設定
コード
下記をコピペしてください。
main.gs
// 共通変数
// 間隔
const RECURRENCE_INTERVAL = {
YEARLY: "年次",
SEMIANNUAL: "半年次",
QUARTERLY: "四半期次",
MONTHLY: "月次",
WEEKLY: "週次",
DAILY: "日次"
}
// 共通関数
const getPropertyFromScript_ = (propertyName) => {
return PropertiesService.getScriptProperties().getProperty(propertyName)
}
const generateDateString_ = (date) => {
return Utilities.formatDate(date, "Asia/Tokyo", "yyyy-MM-dd")
}
const generateDateStringShort_ = (date) => {
// 月日のみで比較する場合に用いる。
return Utilities.formatDate(date, "Asia/Tokyo", "MM-dd")
}
const buildNotionApiOption_ = (method,setPayload) => {
let buf = {
method: method,
contentType: "application/json",
headers: {
"Authorization": `Bearer ${getPropertyFromScript_("NOTION_API_TOKEN")}`,
"Notion-Version": "2022-06-28",
},
muteHttpExceptions: true,
followRedirects: false
}
if (setPayload !== undefined) {
buf.payload = JSON.stringify(setPayload)
}
return buf
}
// 主関数
const main = () => {
// 定期タスク設定DBを取得
const settingDbResults = retrieveNotionSettingDb_()
// タスクの設定をインスタンス化
const settings = generateSettingClasses_(settingDbResults)
// タスクを生成
const result = addTaskToNotionTaskDb_(settings)
// Slackへ通知
sendSlackNotification_(result)
}
const retrieveNotionSettingDb_ = () => {
// 定期タスク設定DBから、DB情報を取得する。
// 発行するがtrueで、バリデーションが通っているものを対象
const query = {
filter: {
and: [{
property: "発行する",
checkbox: {
equals: true
}
},{
property: "Validation",
formula: {
string: {
equals: "Valid"
}
}
}]
}
}
const options = buildNotionApiOption_("get",query)
const url = `https://api.notion.com/v1/databases/${getPropertyFromScript_("NOTION_DB_ID_SETTING")}/query`
const response = UrlFetchApp.fetch(url, options);
const data = JSON.parse(response.getContentText());
return data.results
}
const generateSettingClasses_ = (settingDbResults) => {
// タスクを発行するための設定クラス
class Setting {
constructor(settingDbResult) {
this.settingName = settingDbResult.properties["設定名"].title[0].text.content
this.recurrenceInterval = settingDbResult.properties["間隔"].select.name
this.personInChargeId = settingDbResult.properties["担当者"].people[0].id
this.documentUrl = settingDbResult.properties["説明書URL"].url
this.periodLength = settingDbResult.properties["日数"].number
this.baseDate = settingDbResult.properties["発行基準日付"].date?.start
this.baseDays = settingDbResult.properties["発行基準曜日"].multi_select.map((item) => item.name)
this.canAddTaskOnHoliday = settingDbResult.properties["土日祝も発行する"].checkbox
this.enableSlackNotifications = settingDbResult.properties["通知する"].checkbox
}
}
// 設定をインスタンス化して配列化
const settings = settingDbResults.map((settingDbResult) => new Setting(settingDbResult))
return settings
}
const addTaskToNotionTaskDb_ = (settings) => {
// タスク発行結果を格納する結果クラス
class Result {
constructor(setting) {
this.isSuccess = false
this.settingName = setting.settingName
this.taskName = ""
this.taskUrl = ""
this.personInChargeId = ""
this.errorMessage = ""
}
createPageSuccess(taskName, taskUrl, personInChargeId) {
this.isSuccess = true
this.taskName = taskName
this.taskUrl = taskUrl
this.personInChargeId = personInChargeId
}
createPageFailure(taskName, personInChargeId, errorMessage) {
this.isSuccess = false
this.taskName = taskName.content
this.personInChargeId = personInChargeId
this.errorMessage = errorMessage
}
}
// 処理実行日でタスク生成対象の設定を抽出
const filteredSettings = filterSettings_(settings)
// 設定からタスクを発行してNotionのタスクDBへ追加
const results = filteredSettings.map((setting) => {
let result = new Result(setting)
const page = generatePage_(setting)
const options = buildNotionApiOption_("post",page)
const url = "https://api.notion.com/v1/pages"
// 結果出力のため、ここくらいはエラー処理を入れておく。
try {
const response = UrlFetchApp.fetch(url, options);
const data = JSON.parse(response.getContentText());
if(response.getResponseCode() != 200){
throw new Error(data.message)
}
result.createPageSuccess(
page.properties["タスク名"].title[0].text.content,
data.url,
setting.personInChargeId
)
} catch (e) {
result.createPageFailure(
page.properties["タスク名"].title[0].text.content,
setting.personInChargeId,
e.message
)
}
return result
})
return results
}
const filterSettings_ = (settings) => {
// 本日を取得
const nowDate = new Date()
const nowDateString = generateDateString_(nowDate)
const nowDateStringShort = generateDateStringShort_(nowDate)
// 各間隔タスクを抽出
const filteredYearlySettings = filterSettingsByInterval_(settings, RECURRENCE_INTERVAL.YEARLY, identifySettings_, nowDate, nowDateStringShort);
const filteredSemiAnnualSettings = filterSettingsByInterval_(settings, RECURRENCE_INTERVAL.SEMIANNUAL, identifySettings_, nowDate, nowDateStringShort);
const filteredQuarterlySettings = filterSettingsByInterval_(settings, RECURRENCE_INTERVAL.QUARTERLY, identifySettings_, nowDate, nowDateStringShort);
const filteredMonthlySettings = filterSettingsByInterval_(settings, RECURRENCE_INTERVAL.MONTHLY, identifySettings_, nowDate, nowDateStringShort);
// 週次と日次は別なタスク抽出関数を使用
const filteredWeeklySettings = filterSettingsByInterval_(settings, RECURRENCE_INTERVAL.WEEKLY, identifyWeeklySettings_, nowDate);
const holidays = retrieveHolidays_(nowDate)
const filteredDailySettings = filterSettingsByInterval_(settings, RECURRENCE_INTERVAL.DAILY, identifyDailySettings_, holidays, nowDate, nowDateString);
// 抽出後のタスクを連結
return []
.concat(filteredYearlySettings)
.concat(filteredSemiAnnualSettings)
.concat(filteredQuarterlySettings)
.concat(filteredMonthlySettings)
.concat(filteredWeeklySettings)
.concat(filteredDailySettings)
}
const filterSettingsByInterval_ = (settings, interval, identifyFn, ...args) => {
// 抽出関数の処理を共通化するためのヘルパー関数
return settings
.filter(setting => setting.recurrenceInterval === interval)
.filter(setting => identifyFn(setting, ...args));
}
const identifySettings_ = (setting, nowDate, nowDateStringShort) => {
// 発行対象日を算出
const convertedBaseDates = calculateBaseDates_(setting, nowDate)
// 本日が発行対象日の場合発行
if (convertedBaseDates.includes(nowDateStringShort)) {
return true
}
return false
}
const calculateBaseDates_ = (setting, nowDate) => {
// 処理実行日と設定の「間隔」、各発行基準から算出される発行対象日を算出
const baseDateDetail = generateDateDetail_(Utilities.parseDate(setting.baseDate, "Asia/Tokyo", "yyyy-MM-dd"))
const nowDateDetail = generateDateDetail_(nowDate)
switch (setting.recurrenceInterval) {
case RECURRENCE_INTERVAL.YEARLY:
return [generateDateStringShort_(new Date(nowDateDetail.dateYear, baseDateDetail.dateMonth, baseDateDetail.dateDate))]
case RECURRENCE_INTERVAL.SEMIANNUAL: {
const convertedBaseDate1st = new Date(nowDateDetail.dateYear, baseDateDetail.dateMonth, baseDateDetail.dateDate)
const convertedBaseDate2nd = new Date(nowDateDetail.dateYear, baseDateDetail.dateMonth + 6, baseDateDetail.dateDate)
return [
generateDateStringShort_(convertedBaseDate1st),
generateDateStringShort_(convertedBaseDate2nd)
]
}
case RECURRENCE_INTERVAL.QUARTERLY: {
const convertedBaseDate1st = new Date(nowDateDetail.dateYear, baseDateDetail.dateMonth, baseDateDetail.dateDate)
const convertedBaseDate2nd = new Date(nowDateDetail.dateYear, baseDateDetail.dateMonth + 3, baseDateDetail.dateDate)
const convertedBaseDate3rd = new Date(nowDateDetail.dateYear, baseDateDetail.dateMonth + 6, baseDateDetail.dateDate)
const convertedBaseDate4th = new Date(nowDateDetail.dateYear, baseDateDetail.dateMonth + 9, baseDateDetail.dateDate)
return [
generateDateStringShort_(convertedBaseDate1st),
generateDateStringShort_(convertedBaseDate2nd),
generateDateStringShort_(convertedBaseDate3rd),
generateDateStringShort_(convertedBaseDate4th)
]
}
case RECURRENCE_INTERVAL.MONTHLY:
return [generateDateStringShort_(new Date(nowDateDetail.dateYear, nowDateDetail.dateMonth, baseDateDetail.dateDate))]
default:
return [];
}
}
const generateDateDetail_ = (date) => {
return {
dateYear: date.getFullYear(),
dateMonth: date.getMonth(),
dateDate: date.getDate()
}
}
const identifyWeeklySettings_ = (setting, nowDate) => {
const DAY_NUMBERS = {
"月": 1,
"火": 2,
"水": 3,
"木": 4,
"金": 5,
"土": 6,
"日": 0
}
// 本日の曜日を取得
const nowDateDay = nowDate.getDay()
// 発行基準曜日をインデックスに変換
const convertedBaseDays = setting.baseDays.map((baseDay) => DAY_NUMBERS[baseDay])
// 本日の曜日が発行基準曜日に含まれていれば発行
return convertedBaseDays.includes(nowDateDay)
}
const identifyDailySettings_ = (setting, holidays, nowDate, nowDateString) => {
// 「土日祝も発行する」がtrueの場合は発行
if (setting.canAddTaskOnHoliday) {
return true
}
// 「土日祝も発行する」がfalseで、本日が平日の場合は発行
if (!setting.canAddTaskOnHoliday && !isHoliDay_(holidays, nowDate, nowDateString)) {
return true
}
return false
}
const isHoliDay_ = (holidays, nowDate, nowDateString) => {
// 土日
const nowDay = nowDate.getDay()
if (nowDay == 6 || nowDay == 0) {
return true
}
// 祝日
if (holidays.has(nowDateString)) {
return true
}
return false
}
const retrieveHolidays_ = (nowDate) => {
// Googleカレンダーの日本の祝日カレンダーより、祝日を取得
// toISOString()で世界標準時になってしまうが、イベントは世界標準時で管理されているため問題はない。
const timeMin = new Date(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate(),nowDate.getHours(),nowDate.getMinutes(),nowDate.getSeconds()).toISOString()
const timeMax = new Date(nowDate.getFullYear()+1, nowDate.getMonth(), nowDate.getDate(),nowDate.getHours(),nowDate.getMinutes(),nowDate.getSeconds()).toISOString()
const optionalArgs = {
timeMin: timeMin,
timeMax: timeMax
}
const holidaysResult = Calendar.Events.list("ja.japanese.official#holiday@group.v.calendar.google.com", optionalArgs)
const holidays = new Map()
holidaysResult.items.forEach((holiday) => holidays.set(holiday.start.date, holiday.summary))
return holidays
}
const generatePage_ = (setting) => {
// 発行するタスクのNotionページ用payloadを生成
const nowDate = new Date()
const startDate = generateDateString_(nowDate)
const taskTitle = `${setting.settingName}_${startDate}`
const endDate = generateDateString_(
new Date(
nowDate.getFullYear(),
nowDate.getMonth(),
nowDate.getDate() + setting.periodLength
)
)
return {
parent: {
database_id: getPropertyFromScript_("NOTION_DB_ID_TASK")
},
properties: {
"タスク名": {
title: [
{
text: {
content: taskTitle
}
}
]
},
"担当者": {
people: [
{
object: "user",
id: setting.personInChargeId
}
]
},
"開始日": {
date: {
start: startDate,
end: null
}
},
"終了日": {
date: {
start: endDate,
end: null
}
},
"ステータス": {
status: {
name: "未着手"
}
}
},
children: [
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [
{
type: "text",
text: {
content: "説明書",
link: {
url: setting.documentUrl
}
}
}
]
}
}
]
}
}
const sendSlackNotification_ = (results) => {
// メッセージ生成
const message = generateSlackMessage_(results)
// チャンネルへ投稿
const payload = {
text: message
}
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload)
}
const response = UrlFetchApp.fetch(getPropertyFromScript_("SLACK_WEBHOOK_URL"), options);
}
const generateSlackMessage_ = (results) => {
const nowDateString = Utilities.formatDate(new Date(),"Asia/Tokyo","yyyy-MM-dd")
const users = retrieveUsers_()
const createdTasks = results.map((result) => {
const taskNameString = result.isSuccess ? `<${result.taskUrl}|${result.taskName}>` : `${result.settingName}`
const userName = users.get(result.personInChargeId)
let createdTask =
`${result.isSuccess ? "成功" : "失敗"}:${taskNameString}:${userName}${result.isSuccess ? `` : `:${result.errorMessage}`}`
return createdTask
})
let message = `【${nowDateString}に発行したタスクの作成結果】
`
message += createdTasks.length > 0 ? `${createdTasks.join("\r")}` : `発行したタスクはありません。`
return message
}
const retrieveUsers_ = () => {
const users = new Map()
let hasNext = true
let cursor = undefined
while(hasNext){
const url = cursor ? `https://api.notion.com/v1/users?start_cursor=${cursor}` : "https://api.notion.com/v1/users"
const options = buildNotionApiOption_("get")
const response = UrlFetchApp.fetch(url, options);
const data = JSON.parse(response.getContentText());
data.results.forEach((result)=>users.set(result.id,result.name))
hasNext = data.has_more
cursor = data.next_cursor
}
return users
}
その他
- トリガーで朝の時間帯に日次実行を設定
- 設定すると動き始めるので、実際は使い始めの直前に設定します。
- CalendarAPIを有効
- 日本の祝日カレンダーをGoogleカレンダーに設定してください。日本の祝日をこのカレンダー取得します。
- スクリプトプロパティを設定
- NOTION_API_TOKEN
- NotionのAPIをコールする際に使用するトークンを設定します。
- NOTION_DB_ID_SETTING
- Notionの定期タスク設定DBのIDを設定します。
- NOTION_DB_ID_TASK
- Notionのタスク管理DBのIDを設定します。
- SLACK_WEBHOOK_URL
- 通知先のSlackチャンネル用Webhookをします。
- NOTION_API_TOKEN
準備としては以上です!
実行してみる
試しに下記のようなタスクを設定してみました。
実行日は2025-02-15(土)です。
実行結果はこんな感じ。
Slackの通知はこんな感じ。
これでみなさんのタスクが、少しでも漏れなくて早く確実に完了できるようになることを祈っております!
次回は解説編です(気合いが残っていれば)