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