1. 都道府県別CoderDojoカレンダー
CoderDojoは子どものためのプログラミング道場です。全国各地で開催されています。
開催予定をGoogleカレンダーで見られるようにしました。2025年2月24日現在、30都道府県のカレンダーを公開しています(リンクになっている都道府県です)。
北海道、青森県、岩手県、宮城県、秋田県、山形県、福島県、茨城県、栃木県、群馬県、埼玉県、千葉県、東京都、神奈川県、新潟県、富山県、石川県、福井県、山梨県、長野県、岐阜県、静岡県、愛知県、三重県、滋賀県、京都府、大阪府、兵庫県、奈良県、和歌山県、鳥取県、島根県、岡山県、広島県、山口県、徳島県、香川県、愛媛県、高知県、福岡県、佐賀県、長崎県、熊本県、大分県、宮崎県、鹿児島県、沖縄県
カレンダーを購読すると、自分のGoogleカレンダー上に表示して、スケジュール調整がしやすくなります。購読するには、カレンダーの右下にある「+ Google Calendar」というアイコンをクリックしてください。
最新のカレンダー一覧はこちらのスプレッドシートに掲載しています。
2. カレンダーを作った背景
CoderDojoでは、プログラミングを楽しみたい子どもたちをボランティアのメンターが支援します。
CoderDojo は7〜17歳を主な対象とした非営利のプログラミング道場です。2011年にアイルランドで始まり、世界では100カ国・2,000の道場、日本には216以上の道場があります。CoderDojo は日本各地で毎年1,000回以上開催されており、延べ10,000人以上の子ども達がプログラミングを学んでいます
https://coderdojo.jp/partnership より引用
私も一緒に楽しみたいなと思い、CoderDojoのメンターとして活動しはじめました。今は様々な道場にお邪魔して支援の仕方を学んでいるところです。
全国各地に存在する道場の情報はCoderDojo Japanのサイトに集まっています。直近の開催予定は近日開催の道場で確認できます。
この開催予定を「普段使っているGoogleカレンダーで見たい」と思い、公開カレンダーを自動生成してみました。都道府県別にカレンダーを分けているので、興味のある都道府県だけ購読できます。
@yasulabさんのご協力でcoderdojo.jpのEvent APIが拡張され、CoderDojo Japanと同じ開催予定をカレンダーに掲載できるようになりました
この記事では、自動生成した都道府県別のCoderDojoカレンダーを最初にご紹介します。そして、自動生成に使った技術と、その詳細について解説していきます。
以前は勉強会サービスのconnpassとDoorkeeperでCoderDojo
を検索していました。その時の記事はこちらです。
3. 使っている技術
Google Apps Scriptを毎朝起動し、coderdojo.jpのEvent APIから予定を取得し、公開カレンダーに追加・更新しています。
3.1 coderdojo.jpのEvent APIで開催予定を取得
https://coderdojo.jp/events.json?all_events=true で、その日以降に開催予定のイベントをJSON形式で取得できます。
項目 | 説明 | 例 |
---|---|---|
id | DojoのID。Dojo APIと同じ | 289 |
name | Dojoの名前 | "たまち" |
url | DojoのURL | "https://coderdojo-tamachi.connpass.com/" |
event_id | 開催予定のID | 5722 |
event_title | 開催予定のタイトル | "第8回CoderDojoたまち" |
event_date | 開始時刻 | "2023-07-23T14:00:00.000+09:00" |
event_end_at | 終了時刻 | "2023-07-23T16:30:00.000+09:00" |
event_url | 開催予定のURL | "https://coderdojo-tamachi.connpass.com/event/288359/" |
prefecture | 都道府県 | "東京" |
participants | 参加者数 | 6 |
event_update_at | 更新時刻 | "2023-06-25T19:02:06.000+09:00" |
address | 住所 | "東京都港区芝三丁目41番8号" |
place | 場所 | "駐健保会館 中会議室" |
limit" | 定員数 | 12 |
3.2 Google Apps Scriptを使って無料で実現
実行環境はGoogle Apps Scriptです。いいところがたくさんあります。
- 無料で使える
- 定期実行できるのもありがたいです
- Googleスプレッドシートをデータベース代わりに使います
- GoogleカレンダーやGoogleマップのAPIを呼び出せる
- 共有カレンダーを作るだけでなく、一般に公開することもできます
- Geocoderで緯度経度から都道府県を取得できます(ただし、一定時間内にアクセスできる回数に制限があります)
4. 実装の詳細
APIで予定を検索し、新しい予定はスプレッドシートに追加、更新された予定はスプレッドシートを更新します。新規と更新された予定をカレンダーに設定します。
4.1 開催予定を収集しシートに記録
APIで収集した開催予定は、「イベント一覧」シートに記録します。
4.1.1 APIの呼び出しでエラーが発生した場合にリトライ
APIを呼び出した結果が、正常(HTTPレスポンスコードが200)ではなかった場合、少し時間をおいてリトライすることにします。
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 500;
function fetchWithRetry_(url) {
for (let i = 0; i < MAX_RETRIES; i++) {
const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
if (response.getResponseCode() === 200) {
return response.getContentText();
}
Utilities.sleep(RETRY_DELAY_MS * (i + 1));
}
throw new Error(`Failed to fetch URL: ${url}`);
}
4.1.2 Event APIで開催予定を取得
Event APIから取得できる都道府県名には、都・道・府・県がついていないので、それをつけてあげます。また、住所が空で、タイトルにオンラインがあるものは、オンライン開催としています。
const SPECIAL_PREFECTURES = {
'東京': '東京都',
'北海道': '北海道',
'大阪': '大阪府',
'京都': '京都府'
};
function fetchAllEvents_() {
function onlineIfBlank(prop, title) {
return (!prop && title.includes('オンライン')) ? 'オンライン' : prop;
}
function normalize(event) {
const event_date = new Date(event.event_date);
const event_end_at = new Date(event.event_end_at);
const event_update_at = new Date(event.event_update_at);
const prefecture = SPECIAL_PREFECTURES[event.prefecture] || event.prefecture + '県'
const address = onlineIfBlank(event.address, event.event_title);
const place = onlineIfBlank(event.place, event.event_title);
return {
...event, event_date, event_end_at, event_update_at, prefecture, place, address
};
}
const url = 'https://coderdojo.jp/events.json?all_events=true';
const events = JSON.parse(fetchWithRetry_(url));
return events.map(normalize);
}
4.1.3 収集した開催予定をシートに記録
APIで取得した開催予定は、スプレッドシートに記録します。イベントIDを使ってシートを探し、なければsheet.appendRow()
で追記し、あれば更新日を確認して新しいものならsheet.getRange().setValues()
で更新します。
const SHEET_NAME_CODER_DOJO_EVENTS = 'イベント一覧';
function toEvent_(row) {
const [event_id, prefecture, name, event_title, event_url, event_date, event_end_at, event_update_at, limit, participants, place, address ] = row;
return { event_id, prefecture, name, event_title, event_url, event_date: new Date(event_date), event_end_at: new Date(event_end_at), event_update_at: new Date(event_update_at), limit, participants, place, address };
}
function toRow_(event) {
const { event_id, prefecture, name, event_title, event_url, event_date, event_end_at, event_update_at, limit, participants, place, address } = event;
return [event_id, prefecture, name, event_title, event_url, event_date, event_end_at, event_update_at, limit, participants, place, address];
}
function updateEventsSheet(forceUpdate = false) {
const updatedEvents = [];
const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME_CODER_DOJO_EVENTS);
const oldEvents = sheet.getDataRange().getValues().map(row => toEvent_(row));
fetchAllEvents_().forEach(newEvent => {
const oldEventIndex = oldEvents.findIndex(e => e.event_id === newEvent.event_id);
if (oldEventIndex === -1) {
updatedEvents.push(newEvent);
sheet.appendRow(toRow_(newEvent));
console.log('追加', newEvent);
} else {
const oldEvent = oldEvents[oldEventIndex];
if (forceUpdate || oldEvent.event_update_at < new Date(newEvent.event_update_at)) {
updatedEvents.push(newEvent);
const newEventRow = toRow_(newEvent);
sheet.getRange(oldEventIndex + 1, 1, 1, newEventRow.length).setValues([newEventRow]);
console.log('更新', newEvent);
}
}
});
return updatedEvents;
}
4.2 Googleカレンダーの操作
開催予定の収集が終わったら、その情報をGoogleカレンダーに掲載します。
都道府県別の公開カレンダーは「カレンダー一覧」シートで、カレンダーに掲載する予定は「カレンダーイベント一覧」で、それぞれ管理します。
4.2.1 都道府県別の公開カレンダーを作成
「カレンダー一覧」シートには、都道府県名とカレンダーIDを記載します。
新しい都道府県が見つかった場合、新しくカレンダーを作り、そのカレンダーを公開します。カレンダーを公開するためにCalendar APIでACLを操作します。
const SHEET_NAME_CALENDARS = 'カレンダー一覧';
// https://developers.google.com/apps-script/advanced/calendar?hl=en
// https://developers.google.com/calendar/api/v3/reference/acl?hl=en
function makePublic_(calendarId) {
Calendar.Acl.insert({
kind: 'calendar#aclRule',
role: 'reader',
scope: {
value: '__public_principal__@public.calendar.google.com',
type: 'default'
},
}, calendarId);
}
function fetchOrCreateCalendar_(prefecture) {
function asCalendar_(row) {
const [ prefecture, calendarId, url ] = row;
return { prefecture, calendarId, url };
}
if (!prefecture) throw 'invalid prefecture';
const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME_CALENDARS);
const calendars = sheet.getDataRange().getValues().slice(1).map(row => asCalendar_(row));
const calendar = calendars.find(c => c.prefecture === prefecture);
if (calendar) {
return CalendarApp.getCalendarById(calendar.calendarId);
}
const name = `CoderDojo ${prefecture}`;
const description = `${prefecture}で開催されるCoderDojoの予定です。connpassとDoorkeeperから取得しています。`;
// timeZone を指定しないと GMT になる
const newCalendar = CalendarApp.createCalendar(name, {
color: '#e6e6fa', // lavender
timeZone: 'Asia/Tokyo'
});
newCalendar.setDescription(description);
const newCalendarId = newCalendar.getId();
makePublic_(newCalendarId);
console.log('新しいカレンダーです', name, description);
const url = `https://calendar.google.com/calendar/embed?src=${newCalendarId}&ctz=Asia%2FTokyo`;
sheet.appendRow([prefecture, newCalendarId, url]);
return newCalendar;
}
4.2.2 カレンダーイベントを作成
開催予定に対応するカレンダーイベントを作り、カレンダーに追加します。開催予定のイベントIDと、カレンダーイベントIDの組は予定を更新するときに必要になるため、「カレンダーイベント一覧」に追記しておきます。
const SHEET_NAME_CALENDAR_EVENT_IDS = 'カレンダーイベント一覧';
function getCalendarOption_(event) {
return {
description: [
`URL: ${event.event_url}`,
event.place ? `開催場所: ${event.place}`: '',
`参加人数: ${event.participants}` + ((event.limit > 0) ? ` / ${event.limit}` : ''),
].filter(elem => elem).join('\n'),
location: event.address
}
}
function createCalenderEvent_(calendar, event) {
return calendar.createEvent(
event.event_title,
new Date(event.event_date),
new Date(event.event_end_at),
getCalendarOption_(event)
);
}
function updateCalenderEvent_(calendar, calendarEvenId, event) {
const calendarEvent = calendar.getEventById(calendarEvenId);
calendarEvent.setTitle(event.event_title);
calendarEvent.setTime(event.event_date, event.event_end_at);
const { description, location } = getCalendarOption_(event);
calendarEvent.setDescription(description);
calendarEvent.setLocation(location);
}
let CACHE_CALENDER_EVENTS;
function fetchCalenderEventId_(eventId) {
function asCalendarEvent_(row) {
const [ eventId, calendarId, calendarEventId ] = row;
return { eventId, calendarId, calendarEventId };
}
if (!CACHE_CALENDER_EVENTS) {
CACHE_CALENDER_EVENTS = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME_CALENDAR_EVENT_IDS).getDataRange().getValues().slice(1).map(row => asCalendarEvent_(row));
}
const calendarEvent = CACHE_CALENDER_EVENTS.find(ce => ce.eventId === eventId);
if (!calendarEvent) return null;
return calendarEvent.calendarEventId;
}
function createOrUpdateCalenderEvents_(updatedEvents) {
const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME_CALENDAR_EVENT_IDS);
for (let event of updatedEvents) {
if (!event.prefecture) continue;
const calendar = fetchOrCreateCalendar_(event.prefecture);
const calenderEventId = fetchCalenderEventId_(event.event_id);
if (!calenderEventId) {
const calenderEvent = createCalenderEvent_(calendar, event);
sheet.appendRow([event.event_id, calendar.getId(), calenderEvent.getId()]);
} else {
updateCalenderEvent_(calendar, calenderEventId, event);
}
}
}
4.3 一連の動作を実行
毎朝、APIの検索からカレンダーの更新まで、一連の動作を定期実行します。
更新があった開催予定だけ、カレンダーを更新しています。
function updateEventsSheetAndCalendars(forceUpdate = false) {
const updatedEvents = updateEventsSheet(forceUpdate);
createOrUpdateCalenderEvents_(updatedEvents);
}
5. まとめ
この記事では、CoderDojoの開催予定をGoogleカレンダーで管理する方法について説明しました。具体的には、coderdojo.jpのEvent APIからCoderDojoの開催情報を取得し、それをGoogleカレンダーに自動的に追加する方法を示しました。これにより、CoderDojoの開催情報を一覧で確認しやすくなり、自分のスケジュールとの調整も容易になります。
また、このプロジェクトはGoogle Apps Scriptを使用して実装されており、無料で利用できること、Googleスプレッドシートをデータベース代わりに使用できること、Googleカレンダーを呼び出せることなど、Google Apps Scriptの利点を活かしています。
このプロジェクトはCoderDojoのメンターだけでなく、参加者やその保護者にも役立つと思います。興味のある方はぜひ試してみてください。