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