-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhandler.py
329 lines (272 loc) · 12.6 KB
/
handler.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
import os
import datetime
import jinja2
import webapp2
import logging
import httplib2
from google.appengine.api import users
from apiclient.discovery import build
from google.appengine.api import memcache
from oauth2client.appengine import AppAssertionCredentials
import settings
''' Jinja is a templating language for Python.
Using Jinja, Python code can be embedded into the html file to dynamically construct
html contents.
JINJA_ENVIRONMENT is used to request displaying Jina-based html file.
'''
JINJA_ENVIRONMENT = jinja2.Environment(
loader = jinja2.FileSystemLoader(os.path.join(os.path.dirname(__file__), 'views')),
extensions = ['jinja2.ext.autoescape'],
autoescape = True)
''' The AppAssertionCredentials simplifies OAuth2.0 authentication to Google Calendar API
Using the service accounf of this application
'''
credentials = AppAssertionCredentials(scope=settings.SCOPE)
http = credentials.authorize(httplib2.Http(memcache))
def showError(self, message):
template_values = {
'message': message
}
template = JINJA_ENVIRONMENT.get_template('error.html')
self.response.write(template.render(template_values))
def showWarning(self, message):
template_values = {
'message': message
}
template = JINJA_ENVIRONMENT.get_template('index.html')
self.response.write(template.render(template_values))
""" This is the decorator function which is called just before the function which
has '@auth_required' just before the function definition.
This checks if the current user is in settings.USERS list. """
def auth_required(handler):
def check_login(self, *args, **kwargs):
nickname = users.get_current_user().nickname()
if nickname not in settings.USERS:
showError(self, 'You don\'t have permission to this site. Please ask the administrator.')
else:
return handler(self, *args, **kwargs)
return check_login
""" Handler for the '/' page """
class MainPage(webapp2.RequestHandler):
@auth_required
def get(self):
self.showDashboard()
@auth_required
def post(self):
self.showDashboard()
""" This function shows the dashboard which shows the actual working hour / official working hour for each week of the year,
which is the main feature of this application. """
def showDashboard(self):
user = users.get_current_user()
year_str = self.request.get('year')
current_year = datetime.date.today().year
if not year_str:
year = current_year
else:
year = int(year_str)
if year < 1900 or year > current_year:
showError(self, 'Year should be between 1900 and %d.' % current_year)
return
nickname = None
if users.is_current_user_admin():
nickname = self.request.get('user')
if not nickname or nickname == '':
nickname = user.nickname()
# Get the information from the Google Calendar
week_calendar = self.getCalendar(nickname, year)
template_values = {
'calendar': week_calendar,
'year': year,
'user': nickname,
}
if users.is_current_user_admin():
template_values['admin'] = True
template = JINJA_ENVIRONMENT.get_template('index.html')
self.response.write(template.render(template_values))
# REST request to Google Calendar doesn't work when the app is runnig in the AppEngine SDK environment.
# This function is the stub function which is used to test this app without deploying to the server.
# This function returns fake 'events' similar to the events retrieved from Google Calendar.
def getFakeEvents(self, year):
events = {'items': [
{'location': 'holiday', 'creator': {'email': '[email protected]'}, 'start': {'date': '2015-01-01'}, 'end': {'date': '2015-01-02'}},
{'location': 'holiday', 'creator': {'email': '[email protected]'}, 'start': {'date': '2015-01-06'}, 'end': {'date': '2015-01-08'}},
{'location': 'half', 'creator': {'email': '[email protected]'}, 'start': {'date': '2015-01-08'}, 'end': {'date': '2015-01-09'}},
{'location': 'leave', 'creator': {'email': '[email protected]'}, 'start': {'date': '2015-01-02'}, 'end': {'date': '2015-01-06'}},
{'summary': 'work', 'creator': {'email': '[email protected]'}, 'start': {'dateTime': '2015-01-02T09:00:00Z'}, 'end': {'dateTime': '2015-01-02T17:00:00Z'}},
{'location': 'nolunch', 'creator': {'email': '[email protected]'}, 'start': {'dateTime': '2015-01-06T11:00:00Z'}, 'end': {'dateTime': '2015-01-06T18:00:00Z'}},
{'summary': 'work', 'creator': {'email': '[email protected]'}, 'start': {'dateTime': '2015-01-09T09:00:00Z'}, 'end': {'dateTime': '2015-01-09T15:00:00Z'}},
{'summary': 'work', 'creator': {'email': '[email protected]'}, 'start': {'dateTime': '2015-01-09T09:00:00Z'}, 'end': {'dateTime': '2015-01-09T18:00:00Z'}},
{'location': 'holiday', 'creator': {'email': '[email protected]'}, 'start': {'date': '2015-02-18'}, 'end': {'date': '2015-02-19'}},
{'location': 'holiday', 'creator': {'email': '[email protected]'}, 'start': {'date': '2015-01-16'}, 'end': {'date': '2015-01-17'}},
]}
return events
# This function gets Google Calendar Events and analyze them to construct week_calendar data structure
def getCalendar(self, nickname, year):
holiday_calendar = self.initHolidayCalendar()
week_calendar = self.initWeekCalendar(year)
year_end = datetime.date(year, 12, 31)
one_day = datetime.timedelta(1)
today = datetime.date.today()
# If the nickname is 'test', then use the fake events instead of requesting Google Calendar Service
# Because requesting to Google Calendar doesn't work when this app is running in the AppEngine SDK environment
if self.request.host[0:9] == 'localhost':
events = self.getFakeEvents(year)
else:
if http:
service = build('calendar', 'v3', http=http)
timeMin = str(year) + '-01-01T00:00:00Z'
timeMax = str(year + 1) + '-01-01T00:00:00Z'
request = service.events().list(calendarId = settings.CALENDAR_ID, timeMin = timeMin, timeMax = timeMax)
events = request.execute(http=http)
else:
service = None
events = None
while 1:
if events == None or 'items' not in events:
break
items = events['items']
# Iterate on all the events returned from Google Calendar
for i in range(len(items)):
# Retrieve some relevant values from the event for easy access to those values later in this function
if 'summary' in items[i]:
summary = items[i]['summary'] # Summary (Title) of the event
else:
summary = ''
if 'location' in items[i]: # Location of the event
location = items[i]['location']
else:
location = ''
start = items[i]['start'] # start time (or date) of the event
end = items[i]['end'] # end time (or date) of the event
creator = items[i]['creator'] # Creator of the event
""" If the event is all-day event, it can fall into the following three cases
1. Holiday: The location (or summary) of the event will be 'holiday'
2. Full-day leave: The creator is on (full-day) leave at that day
3. Half-day leave: The creator is on (half-day) leave at that day
This loop checks if the all-day event falls into the above three cases,
and then marks the element of the holiday_calendar array.
The element of the holiday_calendar array can have three values:
0: It is not a holiday (Default)
1: The user is on half-day leave
2: It is the holiday or the user is on full-day leave
It also updates the week_calendar array.
If it is the holiday or full-day leave, then the working hour of that week should be decreased by 8.
If it is the half-day leave, then the workiing hour of that week should be decreased by 4.
"""
if 'date' in start: # Only all-day events will have 'date' field. (Other events will have 'dateTime' field instead.)
month = int(start['date'][5:7])
day = int(start['date'][8:10])
end_date = datetime.date(int(end['date'][0:4]), int(end['date'][5:7]), int(end['date'][8:10]))
# type 0: weekday, type 1: half-day leave type 2: holiday or full-day leave
type = 0
# If the event was created by the current user
if 'email' in creator and creator['email'] == (nickname + "@gmail.com"):
# If the location (or summary) is 'half', it means it is the half-day leave
if summary == 'half' or location == 'half':
type = 1
# Otherwise, it means it is the full-day leave
else:
type = 2
# If the event was not created by another user, and if the location (or summary) is 'holiday'
elif summary == 'holiday' or location == 'holiday':
type = 2
# Update the holiday_calendar and week_calendar accordingly
date = datetime.date(int(start['date'][0:4]), month, day)
while type > 0 and date < end_date and date <= today and date <= year_end:
w = self.getWeekOfYear(year, date)
month = date.month - 1
day = date.day - 1
if w >= 0 and w < len(week_calendar) and date.weekday() <= 4:
if holiday_calendar[month][day] == 0: # If the day was a weekday previously,
week_calendar[w][3] -= type * 4 # then we have to decrease the working hours for that week
holiday_calendar[month][day] = type # and also have to mark it as the holiday.
elif holiday_calendar[month][day] == 1 and type == 2: # If the day was a half-day leave and current event shows that it is holiday or full-day leave,
week_calendar[w][3] -= 4 # then we have to decrease the working hours by 4 for that week
holiday_calendar[month][day] = type # and also have to mark it as the half-day leave
date += one_day
# If the event is not a full-day event and the creator is the current user, then we have to update the week_calendar accordingly
elif ('email' in creator) and (creator['email'] == (nickname + '@gmail.com')):
sdt = self.getDateTimeFromISO(start['dateTime'])
edt = self.getDateTimeFromISO(end['dateTime'])
timedelta = edt - sdt
sd = sdt.date()
w = self.getWeekOfYear(year, sd)
if w >= 0 and w < len(week_calendar):
week_calendar[w][2] += timedelta.total_seconds() / 3600.0 # Increase the actual working hour for that week (by unit of hour)
if sdt.hour <= 12 and edt.hour >= 14 and location != 'nolunch':
week_calendar[w][2] -= 1.0
""" When there are too many events in Google Calendar, then Google Calendar service will send the
'nextPageToken' field. We should ask REST request again with the 'pageToken' field. """
if 'nextPageToken' in events:
pageToken = events['nextPageToken']
request = service.events().list(calendarId = settings.CALENDAR_ID, timeMin = timeMin, timeMax = timeMax, pageToken = pageToken)
events = request.execute(http=http)
continue
break
self.roundWorkingHours(week_calendar)
return week_calendar
def initHolidayCalendar(self):
holiday_calendar = range(12)
for i in range(12):
holiday_calendar[i] = range(31)
for j in range(31):
holiday_calendar[i][j] = 0
return holiday_calendar
def initWeekCalendar(self, year):
week_calendar = range(54)
one_day = datetime.timedelta(1)
today = datetime.date.today()
date = datetime.date(year, 1, 1)
end_date = datetime.date(year, 12, 31)
for i in range(54):
week_calendar[i] = [datetime.date(year, 1, 1), datetime.date(year, 12, 31), 0.0, 0]
w = 0
while date.year == year and date <= today:
if date.weekday() == 0:
week_calendar[w][0] = date
if date.weekday() <= 4:
week_calendar[w][3] += 8
if date.weekday() == 6:
week_calendar[w][1] = date
w += 1
date += one_day
if date.weekday() == 0:
w -= 1
else:
date -= one_day
week_calendar[w][1] = date
return week_calendar[0:w+1]
''' This converts ISO formatted datetime string to datetime data structure.
Please refer to RFC3339 for the format.
example: 2015-01-01T09:00:00Z (Jan 1, 2015, 9 am)
2015-01-01T09:00:00+09:00 (Jan 1, 2015, 9am, GMT+9 time)
'''
def getDateTimeFromISO(self, str):
year = int(str[0:4])
month = int(str[5:7])
day = int(str[8:10])
hour = int(str[11:13])
minute = int(str[14:16])
second = int(str[17:19])
dt = datetime.datetime(year, month, day, hour, minute, second)
return dt
''' This function rounds up the calculated working hours for user-friendly displaying.
(ex. 27.134 --> 27.1)
'''
def roundWorkingHours(self, week_calendar):
for week in week_calendar:
week[2] = round(week[2], 1)
# This function gets the week of year for the given date
def getWeekOfYear(self, year, date):
new_year = datetime.date(year, 1, 1)
monday = new_year - datetime.timedelta(1) * new_year.weekday()
t = date - monday
return (t.days/7)
class Logout(webapp2.RequestHandler):
def get(self):
logout_url = users.create_logout_url('/')
self.redirect(logout_url)
application = webapp2.WSGIApplication([
('/', MainPage),
('/logout', Logout),
], debug=True)