Make gkeep understand list indentation, tweaks to the countdown bar in
[kiosk.git] / gcal_renderer.py
1 from oauth2client.client import AccessTokenRefreshError
2 import constants
3 import datetime
4 import file_writer
5 import gdata
6 import globals
7 import os
8 import renderer
9 import time
10
11 class gcal_renderer(renderer.debuggable_abstaining_renderer):
12     """A renderer to fetch upcoming events from www.google.com/calendar"""
13
14     calendar_whitelist = frozenset([
15         'Alex\'s calendar',
16         'Family',
17         'Holidays in United States',
18         'Lynn Gasch',
19         'Lynn\'s Work',
20         '[email protected]',
21         'Scott Gasch External - Misc',
22         'Birthdays',  # <-- from g+ contacts
23     ])
24
25     class comparable_event(object):
26         """A helper class to sort events."""
27         def __init__(self, start_time, end_time, summary, calendar):
28             if start_time is None:
29                 assert(end_time is None)
30             self.start_time = start_time
31             self.end_time = end_time
32             self.summary = summary
33             self.calendar = calendar
34
35         def __lt__(self, that):
36             if self.start_time is None and that.start_time is None:
37                 return self.summary < that.summary
38             if self.start_time is None or that.start_time is None:
39                 return self.start_time is None
40             return (self.start_time,
41                     self.end_time,
42                     self.summary,
43                     self.calendar) < (that.start_time,
44                                       that.end_time,
45                                       that.summary,
46                                       that.calendar)
47
48         def __str__(self):
49             return '[%s]&nbsp;%s' % (self.timestamp(), self.friendly_name())
50
51         def friendly_name(self):
52             name = self.summary
53             name = name.replace("countdown:", "")
54             return "<B>%s</B>" % name
55
56         def timestamp(self):
57             if self.start_time is None:
58                 return "None"
59             elif (self.start_time.hour == 0):
60                 return datetime.datetime.strftime(self.start_time,
61                                                    '%a %b %d %Y')
62             else:
63                 return datetime.datetime.strftime(self.start_time,
64                                                   '%a %b %d %Y %H:%M%p')
65
66     def __init__(self, name_to_timeout_dict, oauth):
67         super(gcal_renderer, self).__init__(name_to_timeout_dict, True)
68         self.oauth = oauth
69         self.client = self.oauth.calendar_service()
70         self.sortable_events = []
71         self.countdown_events = []
72
73     def debug_prefix(self):
74         return "gcal"
75
76     def periodic_render(self, key):
77         self.debug_print('called for "%s"' % key)
78         if (key == "Render Upcoming Events"):
79             return self.render_upcoming_events()
80         elif (key == "Look For Triggered Events"):
81             return self.look_for_triggered_events()
82         else:
83             raise error('Unexpected operation')
84
85     def render_upcoming_events(self):
86         page_token = None
87         def format_datetime(x):
88             return datetime.datetime.strftime(x, '%Y-%m-%dT%H:%M:%SZ')
89         time_min = datetime.datetime.now()
90         time_max = time_min + datetime.timedelta(95)
91         time_min, time_max = list(map(format_datetime, (time_min, time_max)))
92         self.debug_print("time_min is %s" % time_min)
93         self.debug_print("time_max is %s" % time_max)
94
95         # Writes 2 files:
96         #  + "upcoming events",
97         #  + a countdown timer for a subser of events,
98         f = file_writer.file_writer('gcal_3_none.html')
99         f.write('<h1>Upcoming Calendar Events:</h1><hr>\n')
100         f.write('<center><table width=96%>\n')
101
102         g = file_writer.file_writer('countdown_3_7200.html')
103         g.write('<h1>Countdowns:</h1><hr><ul>\n')
104
105         try:
106             self.sortable_events = []
107             self.countdown_events = []
108             while True:
109                 calendar_list = self.client.calendarList().list(
110                     pageToken=page_token).execute()
111                 for calendar in calendar_list['items']:
112                     if (calendar['summary'] in gcal_renderer.calendar_whitelist):
113                         events = self.client.events().list(
114                             calendarId=calendar['id'],
115                             singleEvents=True,
116                             timeMin=time_min,
117                             timeMax=time_max,
118                             maxResults=50).execute()
119
120                         def parse_date(x):
121                             y = x.get('date')
122                             if y:
123                                 y = datetime.datetime.strptime(y, '%Y-%m-%d')
124                             else:
125                                 y = x.get('dateTime')
126                                 if y:
127                                     y = datetime.datetime.strptime(y[:-6],
128                                                          '%Y-%m-%dT%H:%M:%S')
129                                 else:
130                                     y = None
131                             return y
132
133                         for event in events['items']:
134                             try:
135                                 summary = event['summary']
136                                 self.debug_print("event '%s' (%s to %s)" % (
137                                     summary, event['start'], event['end']))
138                                 start = parse_date(event['start'])
139                                 end = parse_date(event['end'])
140                                 self.sortable_events.append(
141                                     gcal_renderer.comparable_event(start,
142                                                                    end,
143                                                                    summary,
144                                                                    calendar['summary']))
145                                 if ('countdown' in summary or
146                                     'Holidays' in calendar['summary'] or
147                                     'Countdown' in summary):
148                                     self.debug_print("event is countdown worthy")
149                                     self.countdown_events.append(
150                                         gcal_renderer.comparable_event(start,
151                                                                        end,
152                                                                        summary,
153                                                                        calendar['summary']))
154                             except Exception as e:
155                                 print("gcal unknown exception, skipping event.");
156                     else:
157                         self.debug_print("Skipping calendar '%s'" % calendar['summary'])
158                 page_token = calendar_list.get('nextPageToken')
159                 if not page_token: break
160
161             self.sortable_events.sort()
162             upcoming_sortable_events = self.sortable_events[:12]
163             for event in upcoming_sortable_events:
164                 self.debug_print("sorted event: %s" % event.friendly_name())
165                 f.write("""
166 <tr>
167   <td style="padding-right: 1em;">
168     %s
169   </td>
170   <td style="padding-left: 1em;">
171     %s
172   </td>
173 </tr>\n""" % (event.timestamp(), event.friendly_name()))
174             f.write('</table></center>\n')
175             f.close()
176
177             self.countdown_events.sort()
178             upcoming_countdown_events = self.countdown_events[:12]
179             now = datetime.datetime.now()
180             count = 0
181             timestamps = { }
182             for event in upcoming_countdown_events:
183                 eventstamp = event.start_time
184                 delta = eventstamp - now
185                 name = event.friendly_name()
186                 x = int(delta.total_seconds())
187                 if x > 0:
188                     identifier = "id%d" % count
189                     days = divmod(x, constants.seconds_per_day)
190                     hours = divmod(days[1], constants.seconds_per_hour)
191                     minutes = divmod(hours[1], constants.seconds_per_minute)
192                     g.write('<li><SPAN id="%s">%d days, %02d:%02d</SPAN> until %s</li>\n' % (identifier, days[0], hours[0], minutes[0], name))
193                     timestamps[identifier] = time.mktime(eventstamp.timetuple())
194                     count += 1
195                     self.debug_print("countdown to %s is %dd %dh %dm" % (
196                         name, days[0], hours[0], minutes[0]))
197             g.write('</ul>')
198             g.write('<SCRIPT>\nlet timestampMap = new Map([')
199             for x in list(timestamps.keys()):
200                 g.write('    ["%s", %f],\n' % (x, timestamps[x] * 1000.0))
201             g.write(']);\n\n')
202             g.write("""
203 // Pad things with a leading zero if necessary.
204 function pad(n) {
205     return (n < 10) ? ("0" + n) : n;
206 }
207
208 // Return an 's' if things are plural.
209 function plural(n) {
210     return (n == 1) ? "" : "s";
211 }
212
213 // Periodic function to run the page timers.
214 var fn = setInterval(function() {
215     var now = new Date().getTime();
216     for (let [id, timestamp] of timestampMap) {
217         var delta = timestamp - now;
218
219         if (delta > 0) {
220             var days = Math.floor(delta / (1000 * 60 * 60 * 24));
221             var hours = pad(Math.floor((delta % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)));
222             var minutes = pad(Math.floor((delta % (1000 * 60 * 60)) / (1000 * 60)));
223             var seconds = pad(Math.floor((delta % (1000 * 60)) / 1000));
224
225             var s = days + " day" + plural(days) + ", ";
226             s = s + hours + ":" + minutes;
227             document.getElementById(id).innerHTML = s;
228         } else {
229             document.getElementById(id).innerHTML = "EXPIRED";
230         }
231     }
232 }, 1000);
233 </script>""");
234             g.close()
235             return True
236         except (gdata.service.RequestError, AccessTokenRefreshError):
237             print("********* TRYING TO REFRESH GCAL CLIENT *********")
238             self.oauth.refresh_token()
239             self.client = self.oauth.calendar_service()
240             return False
241         except:
242             raise
243
244     def look_for_triggered_events(self):
245         f = file_writer.file_writer(constants.gcal_imminent_pagename)
246         f.write('<h1>Imminent Upcoming Calendar Events:</h1>\n<hr>\n')
247         f.write('<center><table width=99%>\n')
248         now = datetime.datetime.now()
249         count = 0
250         for event in self.sortable_events:
251             eventstamp = event.start_time
252             delta = eventstamp - now
253             x = int(delta.total_seconds())
254             if x > 0 and x <= constants.seconds_per_minute * 3:
255                 days = divmod(x, constants.seconds_per_day)
256                 hours = divmod(days[1], constants.seconds_per_hour)
257                 minutes = divmod(hours[1], constants.seconds_per_minute)
258                 eventstamp = event.start_time
259                 name = event.friendly_name()
260                 calendar = event.calendar
261                 f.write("<LI> %s (%s) upcoming in %d minutes.\n" % (name, calendar, minutes[0]))
262                 count += 1
263         f.write("</table>")
264         f.close()
265         if count > 0:
266             globals.put("gcal_triggered", True)
267         else:
268             globals.put("gcal_triggered", False)
269         return True