3 """Renders an upcoming events page and countdowns page based on the
4 contents of several Google calendars."""
9 from oauth2client.client import AccessTokenRefreshError
12 from typing import Dict, List, Tuple
21 class gcal_renderer(renderer.debuggable_abstaining_renderer):
22 """A renderer to fetch upcoming events from www.google.com/calendar"""
24 calendar_whitelist = frozenset(
28 "Holidays in United States",
32 "Scott Gasch External - Misc",
33 "Birthdays", # <-- from g+ contacts
37 class comparable_event(object):
38 """A helper class to sort events."""
42 start_time: datetime.datetime,
43 end_time: datetime.datetime,
47 if start_time is None:
48 assert end_time is None
49 self.start_time = start_time
50 self.end_time = end_time
51 self.summary = summary
52 self.calendar = calendar
54 def __lt__(self, that) -> bool:
55 if self.start_time is None and that.start_time is None:
56 return self.summary < that.summary
57 if self.start_time is None or that.start_time is None:
58 return self.start_time is None
59 return (self.start_time, self.end_time, self.summary, self.calendar) < (
66 def __str__(self) -> str:
67 return "[%s] %s" % (self.timestamp(), self.friendly_name())
69 def friendly_name(self) -> str:
71 name = name.replace("countdown:", "")
72 return "<B>%s</B>" % name
74 def timestamp(self) -> str:
75 if self.start_time is None:
77 elif self.start_time.hour == 0:
78 return datetime.datetime.strftime(self.start_time, "%a %b %d %Y")
80 return datetime.datetime.strftime(
81 self.start_time, "%a %b %d %Y %H:%M%p"
85 self, name_to_timeout_dict: Dict[str, int], oauth: gdata_oauth.OAuth
87 super(gcal_renderer, self).__init__(name_to_timeout_dict, True)
89 self.client = self.oauth.calendar_service()
90 self.sortable_events = []
91 self.countdown_events = []
93 def debug_prefix(self) -> str:
96 def periodic_render(self, key: str) -> bool:
97 self.debug_print('called for "%s"' % key)
98 if key == "Render Upcoming Events":
99 return self.render_upcoming_events()
100 elif key == "Look For Triggered Events":
101 return self.look_for_triggered_events()
103 raise error("Unexpected operation")
105 def get_min_max_timewindow(self) -> Tuple[str, str]:
106 now = datetime.datetime.now()
107 time_min = now - datetime.timedelta(1)
108 time_max = now + datetime.timedelta(95)
109 time_min, time_max = list(
111 lambda x: datetime.datetime.strftime(x, "%Y-%m-%dT%H:%M:%SZ"),
112 (time_min, time_max),
115 print(type(time_min))
116 self.debug_print("time_min is %s" % time_min)
117 self.debug_print("time_max is %s" % time_max)
118 return (time_min, time_max)
121 def parse_date(date_str: str) -> datetime.datetime:
124 _ = date_str.get("date")
126 retval = datetime.datetime.strptime(_, "%Y-%m-%d")
128 _ = date_str.get("dateTime")
130 retval = datetime.datetime.strptime(_[:-6], "%Y-%m-%dT%H:%M:%S")
136 def get_events_from_interesting_calendars(
137 self, time_min: str, time_max: str
138 ) -> Tuple[List[comparable_event], List[comparable_event]]:
141 countdown_events = []
144 self.client.calendarList().list(pageToken=page_token).execute()
146 for calendar in calendar_list["items"]:
147 if calendar["summary"] in gcal_renderer.calendar_whitelist:
149 f"{calendar['summary']} is an interesting calendar..."
154 calendarId=calendar["id"],
162 for event in events["items"]:
163 summary = event["summary"]
165 f" ... event '{summary}' ({event['start']} to {event['end']}"
167 start = gcal_renderer.parse_date(event["start"])
168 end = gcal_renderer.parse_date(event["end"])
169 if start is not None and end is not None:
170 sortable_events.append(
171 gcal_renderer.comparable_event(
172 start, end, summary, calendar["summary"]
176 "countdown" in summary
177 or "Holidays" in calendar["summary"]
178 or "Countdown" in summary
180 self.debug_print(" ... event is countdown worthy!")
181 countdown_events.append(
182 gcal_renderer.comparable_event(
183 start, end, summary, calendar["summary"]
186 page_token = calendar_list.get("nextPageToken")
189 return (sortable_events, countdown_events)
191 def render_upcoming_events(self) -> bool:
192 (time_min, time_max) = self.get_min_max_timewindow()
194 # Populate the "Upcoming Events" page.
196 self.sortable_events,
197 self.countdown_events,
198 ) = self.get_events_from_interesting_calendars(time_min, time_max)
199 self.sortable_events.sort()
200 with file_writer.file_writer("gcal_3_86400.html") as f:
201 f.write("<h1>Upcoming Calendar Events:</h1><hr>\n")
202 f.write("<center><table width=96%>\n")
203 upcoming_sortable_events = self.sortable_events[:12]
204 for event in upcoming_sortable_events:
208 <td style="padding-right: 1em;">
211 <td style="padding-left: 1em;">
212 {event.friendly_name()}
216 f.write("</table></center>\n")
218 # Populate the "Countdown" page.
219 self.countdown_events.sort()
220 with file_writer.file_writer("countdown_3_7200.html") as g:
221 g.write("<h1>Countdowns:</h1><hr><ul>\n")
222 now = datetime.datetime.now()
223 upcoming_countdown_events = self.countdown_events[:12]
226 for event in upcoming_countdown_events:
227 eventstamp = event.start_time
228 name = event.friendly_name()
229 delta = eventstamp - now
230 x = int(delta.total_seconds())
232 identifier = "id%d" % count
233 days = divmod(x, constants.seconds_per_day)
234 hours = divmod(days[1], constants.seconds_per_hour)
235 minutes = divmod(hours[1], constants.seconds_per_minute)
237 f'<li><SPAN id="%s">%d days, %02d:%02d</SPAN> until %s</li>\n'
246 timestamps[identifier] = time.mktime(eventstamp.timetuple())
249 "countdown to %s is %dd %dh %dm"
250 % (name, days[0], hours[0], minutes[0])
253 g.write("<SCRIPT>\nlet timestampMap = new Map([")
254 for x in list(timestamps.keys()):
255 g.write(f' ["{x}", {timestamps[x] * 1000.0}],\n')
259 // Pad things with a leading zero if necessary.
261 return (n < 10) ? ("0" + n) : n;
264 // Return an 's' if things are plural.
266 return (n == 1) ? "" : "s";
269 // Periodic function to run the page timers.
270 var fn = setInterval(function() {
271 var now = new Date().getTime();
272 for (let [id, timestamp] of timestampMap) {
273 var delta = timestamp - now;
276 var days = Math.floor(delta / (1000 * 60 * 60 * 24));
277 var hours = pad(Math.floor((delta % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)));
278 var minutes = pad(Math.floor((delta % (1000 * 60 * 60)) / (1000 * 60)));
279 var seconds = pad(Math.floor((delta % (1000 * 60)) / 1000));
281 var s = days + " day" + plural(days) + ", ";
282 s = s + hours + ":" + minutes;
283 document.getElementById(id).innerHTML = s;
285 document.getElementById(id).innerHTML = "EXPIRED";
292 except (gdata.service.RequestError, AccessTokenRefreshError):
293 print("********* TRYING TO REFRESH GCAL CLIENT *********")
294 self.oauth.refresh_token()
295 self.client = self.oauth.calendar_service()
300 def look_for_triggered_events(self) -> bool:
301 with file_writer.file_writer(constants.gcal_imminent_pagename) as f:
302 f.write("<h1>Imminent Upcoming Calendar Events:</h1>\n<hr>\n")
303 f.write("<center><table width=99%>\n")
304 now = datetime.datetime.now()
306 for event in self.sortable_events:
307 eventstamp = event.start_time
308 delta = eventstamp - now
309 x = int(delta.total_seconds())
310 if x > 0 and x <= constants.seconds_per_minute * 3:
311 days = divmod(x, constants.seconds_per_day)
312 hours = divmod(days[1], constants.seconds_per_hour)
313 minutes = divmod(hours[1], constants.seconds_per_minute)
314 eventstamp = event.start_time
315 name = event.friendly_name()
316 calendar = event.calendar
318 f"<LI> {name} ({calendar}) upcoming in {int(minutes[0])} minutes.\n"
323 globals.put("gcal_triggered", True)
325 globals.put("gcal_triggered", False)
330 # oauth = gdata_oauth.OAuth(secrets.google_client_id, secrets.google_client_secret)
332 # {"Render Upcoming Events": 10000, "Look For Triggered Events": 1},
334 # x.periodic_render("Render Upcoming Events")