OLD | NEW |
| (Empty) |
1 # Copyright 2014 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 | |
5 import calendar | |
6 import datetime | |
7 import json | |
8 import logging | |
9 import webapp2 | |
10 import zlib | |
11 | |
12 from google.appengine.api import memcache | |
13 from google.appengine.api import users | |
14 from google.appengine.datastore import datastore_query | |
15 from google.appengine.ext import ndb | |
16 | |
17 LOGGER = logging.getLogger(__name__) | |
18 | |
19 | |
20 class DateTimeEncoder(json.JSONEncoder): | |
21 def default(self, obj): | |
22 if isinstance(obj, datetime.datetime): | |
23 return calendar.timegm(obj.timetuple()) | |
24 # Let the base class default method raise the TypeError. | |
25 return json.JSONEncoder.default(self, obj) | |
26 | |
27 | |
28 class AlertsJSON(ndb.Model): | |
29 type = ndb.StringProperty() | |
30 json = ndb.BlobProperty(compressed=True) | |
31 date = ndb.DateTimeProperty(auto_now_add=True) | |
32 | |
33 | |
34 class AlertsHandler(webapp2.RequestHandler): | |
35 ALERTS_TYPE = 'alerts' | |
36 | |
37 # Has no 'response' member. | |
38 # pylint: disable=E1101 | |
39 def send_json_headers(self): | |
40 self.response.headers.add_header('Access-Control-Allow-Origin', '*') | |
41 self.response.headers['Content-Type'] = 'application/json' | |
42 | |
43 # Has no 'response' member. | |
44 # pylint: disable=E1101 | |
45 def send_json_data(self, data): | |
46 self.send_json_headers() | |
47 self.response.write(data) | |
48 | |
49 def generate_json_dump(self, alerts): | |
50 return json.dumps(alerts, cls=DateTimeEncoder, indent=1) | |
51 | |
52 def get_from_memcache(self, memcache_key): | |
53 compressed = memcache.get(memcache_key) | |
54 if not compressed: | |
55 self.send_json_headers() | |
56 return | |
57 uncompressed = zlib.decompress(compressed) | |
58 self.send_json_data(uncompressed) | |
59 | |
60 def get(self): | |
61 self.get_from_memcache(AlertsHandler.ALERTS_TYPE) | |
62 | |
63 def post_to_history(self, alerts_type, alerts): | |
64 last_query = AlertsJSON.query().filter(AlertsJSON.type == alerts_type) | |
65 last_entry = last_query.order(-AlertsJSON.date).get() | |
66 last_alerts = json.loads(last_entry.json) if last_entry else {} | |
67 | |
68 # Only changes to the fields with 'alerts' in the name should cause a | |
69 # new history entry to be saved. | |
70 def alert_fields(alerts_json): | |
71 filtered_json = {} | |
72 for key, value in alerts_json.iteritems(): | |
73 if 'alerts' in key: | |
74 filtered_json[key] = value | |
75 return filtered_json | |
76 | |
77 if alert_fields(last_alerts) != alert_fields(alerts): | |
78 new_entry = AlertsJSON( | |
79 json=self.generate_json_dump(alerts), | |
80 type=alerts_type) | |
81 new_entry.put() | |
82 | |
83 # Has no 'response' member. | |
84 # pylint: disable=E1101 | |
85 def post_to_memcache(self, memcache_key, alerts): | |
86 uncompressed = self.generate_json_dump(alerts) | |
87 compression_level = 1 | |
88 compressed = zlib.compress(uncompressed, compression_level) | |
89 memcache.set(memcache_key, compressed) | |
90 | |
91 def parse_alerts(self, alerts_json): | |
92 try: | |
93 alerts = json.loads(alerts_json) | |
94 except ValueError: | |
95 warning = 'content field was not JSON' | |
96 self.response.set_status(400, warning) | |
97 LOGGER.warn(warning) | |
98 return | |
99 | |
100 alerts.update({'date': datetime.datetime.utcnow()}) | |
101 | |
102 return alerts | |
103 | |
104 def update_alerts(self, alerts_type): | |
105 alerts = self.parse_alerts(self.request.get('content')) | |
106 if alerts: | |
107 self.post_to_memcache(alerts_type, alerts) | |
108 self.post_to_history(alerts_type, alerts) | |
109 | |
110 def post(self): | |
111 self.update_alerts(AlertsHandler.ALERTS_TYPE) | |
112 | |
113 | |
114 class AlertsHistory(webapp2.RequestHandler): | |
115 MAX_LIMIT_PER_PAGE = 100 | |
116 | |
117 def get_entry(self, query, key): | |
118 try: | |
119 key = int(key) | |
120 except ValueError: | |
121 self.response.set_status(400, 'Invalid key format') | |
122 return {} | |
123 | |
124 ndb_key = ndb.Key(AlertsJSON, key) | |
125 result = query.filter(AlertsJSON.key == ndb_key).get() | |
126 if result: | |
127 return json.loads(result.json) | |
128 else: | |
129 self.response.set_status(404, 'Failed to find key %s' % key) | |
130 return {} | |
131 | |
132 def get_list(self, query): | |
133 cursor = self.request.get('cursor') | |
134 if cursor: | |
135 cursor = datastore_query.Cursor(urlsafe=cursor) | |
136 | |
137 limit = int(self.request.get('limit', self.MAX_LIMIT_PER_PAGE)) | |
138 limit = min(self.MAX_LIMIT_PER_PAGE, limit) | |
139 | |
140 if cursor: | |
141 alerts, next_cursor, has_more = query.fetch_page(limit, | |
142 start_cursor=cursor
) | |
143 else: | |
144 alerts, next_cursor, has_more = query.fetch_page(limit) | |
145 | |
146 return { | |
147 'has_more': has_more, | |
148 'cursor': next_cursor.urlsafe() if next_cursor else '', | |
149 'history': [alert.key.integer_id() for alert in alerts] | |
150 } | |
151 | |
152 def get(self, key=None): | |
153 query = AlertsJSON.query().order(-AlertsJSON.date) | |
154 result_json = {} | |
155 | |
156 user = users.get_current_user() | |
157 result_json['login-url'] = users.create_login_url(self.request.uri) | |
158 | |
159 # Return only public alerts for non-internal users. | |
160 if not user or not user.email().endswith('@google.com'): | |
161 query = query.filter(AlertsJSON.type == AlertsHandler.ALERTS_TYPE) | |
162 | |
163 if key: | |
164 result_json.update(self.get_entry(query, key)) | |
165 else: | |
166 result_json.update(self.get_list(query)) | |
167 | |
168 self.response.headers['Content-Type'] = 'application/json' | |
169 self.response.out.write(json.dumps(result_json)) | |
170 | |
171 | |
172 app = webapp2.WSGIApplication([ | |
173 ('/alerts', AlertsHandler), | |
174 ('/alerts-history', AlertsHistory), | |
175 ('/alerts-history/(.*)', AlertsHistory), | |
176 ]) | |
OLD | NEW |