LoginSignup
4
1

都道府県別のCoderDojoカレンダーをGoogle Apps Scriptで自動生成

Last updated at Posted at 2023-07-09

1. 都道府県別CoderDojoカレンダー

CoderDojoは子どものためのプログラミング道場です。全国各地で開催されています。

開催予定をGoogleカレンダーで見られるようにしました。2023年8月5日現在、28都道府県のカレンダーを公開しています(リンクになっている都道府県です)。

北海道青森県岩手県宮城県、秋田県、山形県福島県茨城県栃木県、群馬県、埼玉県千葉県東京都神奈川県、新潟県、富山県、石川県、福井県、山梨県、長野県岐阜県静岡県愛知県、三重県、滋賀県、京都府大阪府兵庫県奈良県、和歌山県、鳥取県、島根県、岡山県広島県山口県、徳島県、香川県、愛媛県、高知県、福岡県、佐賀県、長崎県、熊本県、大分県、宮崎県、鹿児島県沖縄県

カレンダーを購読すると、自分のGoogleカレンダー上に表示して、スケジュール調整がしやすくなります。購読するには、カレンダーの右下にある「+ Google Calendar」というアイコンをクリックしてください。
+ 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カレンダーで見たい」と思い、公開カレンダーを自動生成してみました。都道府県別にカレンダーを分けているので、興味のある都道府県だけ購読できます。

スクリーンショット 2023-07-09 15.56.05.png

@yasulabさんのご協力でcoderdojo.jpのEvent APIが拡張され、CoderDojo Japanと同じ開催予定をカレンダーに掲載できるようになりました :clap:

この記事では、自動生成した都道府県別のCoderDojoカレンダーを最初にご紹介します。そして、自動生成に使った技術と、その詳細について解説していきます。

以前は勉強会サービスのconnpassDoorkeeperCoderDojoを検索していました。その時の記事はこちらです。

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を呼び出せる

4. 実装の詳細

APIで予定を検索し、新しい予定はスプレッドシートに追加、更新された予定はスプレッドシートを更新します。新規と更新された予定をカレンダーに設定します。

4.1 開催予定を収集しシートに記録

APIで収集した開催予定は、「イベント一覧」シートに記録します。

4.1.1 APIの呼び出しでエラーが発生した場合にリトライ

APIを呼び出した結果が、正常(HTTPレスポンスコードが200)ではなかった場合、少し時間をおいてリトライすることにします。

CoderDojoEvents.gs
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から取得できる都道府県名には、都・道・府・県がついていないので、それをつけてあげます。また、住所が空で、タイトルにオンラインがあるものは、オンライン開催としています。

CoderDojoEvents.gs
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()で更新します。

CoderDojoEvents.gs
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を操作します。

Calendar.gs
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の組は予定を更新するときに必要になるため、「カレンダーイベント一覧」に追記しておきます。

Code.gs
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の検索からカレンダーの更新まで、一連の動作を定期実行します。

更新があった開催予定だけ、カレンダーを更新しています。

Code.gs
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のメンターだけでなく、参加者やその保護者にも役立つと思います。興味のある方はぜひ試してみてください。

4
1
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
4
1