3 """Renders an upcoming events page and countdowns page based on the
4 contents of several Google calendars."""
7 import gdata # type: ignore
9 from oauth2client.client import AccessTokenRefreshError # type: ignore
12 from typing import Dict, List, Optional, 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: Optional[datetime.datetime],
43 end_time: Optional[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: List[gcal_renderer.comparable_event] = []
91 self.countdown_events: List[gcal_renderer.comparable_event] = []
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 Exception("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 = datetime.datetime.strftime(_time_min, "%Y-%m-%dT%H:%M:%SZ")
110 time_max = datetime.datetime.strftime(_time_max, "%Y-%m-%dT%H:%M:%SZ")
111 self.debug_print(f"time_min is {time_min}")
112 self.debug_print(f"time_max is {time_max}")
113 return (time_min, time_max)
116 def parse_date(date_str: str) -> Optional[datetime.datetime]:
119 _ = date_str.get("date")
121 retval = datetime.datetime.strptime(_, "%Y-%m-%d")
123 _ = date_str.get("dateTime")
125 retval = datetime.datetime.strptime(_[:-6], "%Y-%m-%dT%H:%M:%S")
131 def get_events_from_interesting_calendars(
132 self, time_min: str, time_max: str
133 ) -> Tuple[List[comparable_event], List[comparable_event]]:
136 countdown_events = []
139 self.client.calendarList().list(pageToken=page_token).execute()
141 for calendar in calendar_list["items"]:
142 if calendar["summary"] in gcal_renderer.calendar_whitelist:
144 f"{calendar['summary']} is an interesting calendar..."
149 calendarId=calendar["id"],
157 for event in events["items"]:
158 summary = event["summary"]
160 f" ... event '{summary}' ({event['start']} to {event['end']}"
162 start = gcal_renderer.parse_date(event["start"])
163 end = gcal_renderer.parse_date(event["end"])
164 if start is not None and end is not None:
165 sortable_events.append(
166 gcal_renderer.comparable_event(
167 start, end, summary, calendar["summary"]
171 "countdown" in summary
172 or "Holidays" in calendar["summary"]
173 or "Countdown" in summary
175 self.debug_print(" ... event is countdown worthy!")
176 countdown_events.append(
177 gcal_renderer.comparable_event(
178 start, end, summary, calendar["summary"]
181 page_token = calendar_list.get("nextPageToken")
184 return (sortable_events, countdown_events)
186 def render_upcoming_events(self) -> bool:
187 (time_min, time_max) = self.get_min_max_timewindow()
189 # Populate the "Upcoming Events" page.
191 self.sortable_events,
192 self.countdown_events,
193 ) = self.get_events_from_interesting_calendars(time_min, time_max)
194 self.sortable_events.sort()
195 with file_writer.file_writer("gcal_3_86400.html") as f:
196 f.write("<h1>Upcoming Calendar Events:</h1><hr>\n")
197 f.write("<center><table width=96%>\n")
198 upcoming_sortable_events = self.sortable_events[:12]
199 for event in upcoming_sortable_events:
203 <td style="padding-right: 1em;">
206 <td style="padding-left: 1em;">
207 {event.friendly_name()}
211 f.write("</table></center>\n")
213 # Populate the "Countdown" page.
214 self.countdown_events.sort()
215 with file_writer.file_writer("countdown_3_7200.html") as g:
216 g.write("<h1>Countdowns:</h1><hr><ul>\n")
217 now = datetime.datetime.now()
218 upcoming_countdown_events = self.countdown_events[:12]
221 for event in upcoming_countdown_events:
222 eventstamp = event.start_time
223 if eventstamp is None:
225 name = event.friendly_name()
226 delta = eventstamp - now
227 x = int(delta.total_seconds())
229 identifier = "id%d" % count
230 days = divmod(x, constants.seconds_per_day)
231 hours = divmod(days[1], constants.seconds_per_hour)
232 minutes = divmod(hours[1], constants.seconds_per_minute)
234 f'<li><SPAN id="%s">%d days, %02d:%02d</SPAN> until %s</li>\n'
243 timestamps[identifier] = time.mktime(eventstamp.timetuple())
246 "countdown to %s is %dd %dh %dm"
247 % (name, days[0], hours[0], minutes[0])
250 g.write("<SCRIPT>\nlet timestampMap = new Map([")
251 for _ in list(timestamps.keys()):
252 g.write(f' ["{_}", {timestamps[_] * 1000.0}],\n')
256 // Pad things with a leading zero if necessary.
258 return (n < 10) ? ("0" + n) : n;
261 // Return an 's' if things are plural.
263 return (n == 1) ? "" : "s";
266 // Periodic function to run the page timers.
267 var fn = setInterval(function() {
268 var now = new Date().getTime();
269 for (let [id, timestamp] of timestampMap) {
270 var delta = timestamp - now;
273 var days = Math.floor(delta / (1000 * 60 * 60 * 24));
274 var hours = pad(Math.floor((delta % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)));
275 var minutes = pad(Math.floor((delta % (1000 * 60 * 60)) / (1000 * 60)));
276 var seconds = pad(Math.floor((delta % (1000 * 60)) / 1000));
278 var s = days + " day" + plural(days) + ", ";
279 s = s + hours + ":" + minutes;
280 document.getElementById(id).innerHTML = s;
282 document.getElementById(id).innerHTML = "EXPIRED";
289 except (gdata.service.RequestError, AccessTokenRefreshError):
290 print("********* TRYING TO REFRESH GCAL CLIENT *********")
291 self.oauth.refresh_token()
292 self.client = self.oauth.calendar_service()
297 def look_for_triggered_events(self) -> bool:
298 with file_writer.file_writer(constants.gcal_imminent_pagename) as f:
299 f.write("<h1>Imminent Upcoming Calendar Events:</h1>\n<hr>\n")
300 f.write("<center><table width=99%>\n")
301 now = datetime.datetime.now()
303 for event in self.sortable_events:
304 eventstamp = event.start_time
305 if eventstamp is None:
307 delta = eventstamp - now
308 x = int(delta.total_seconds())
309 if x > 0 and x <= constants.seconds_per_minute * 3:
310 days = divmod(x, constants.seconds_per_day)
311 hours = divmod(days[1], constants.seconds_per_hour)
312 minutes = divmod(hours[1], constants.seconds_per_minute)
313 eventstamp = event.start_time
314 name = event.friendly_name()
315 calendar = event.calendar
317 f"<LI> {name} ({calendar}) upcoming in {int(minutes[0])} minutes.\n"
322 globals.put("gcal_triggered", True)
324 globals.put("gcal_triggered", False)
329 # oauth = gdata_oauth.OAuth(secrets.google_client_id, secrets.google_client_secret)
331 # {"Render Upcoming Events": 10000, "Look For Triggered Events": 1},
333 # x.periodic_render("Render Upcoming Events")