| 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 type = ndb.StringProperty() |
| 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_TYPE = '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_TYPE) |
| 59 | 62 |
| 60 def save_alerts_to_history(self, alerts): | 63 def post_to_history(self, alerts_type, alerts): |
| 61 last_entry = AlertsJSON.query().order(-AlertsJSON.date).get() | 64 last_query = AlertsJSON.query().filter(AlertsJSON.type == alerts_type) |
| 65 last_entry = last_query.order(-AlertsJSON.date).get() |
| 62 last_alerts = json.loads(last_entry.json) if last_entry else {} | 66 last_alerts = json.loads(last_entry.json) if last_entry else {} |
| 63 | 67 |
| 64 # Only changes to the fields with 'alerts' in the name should cause a | 68 # Only changes to the fields with 'alerts' in the name should cause a |
| 65 # new history entry to be saved. | 69 # new history entry to be saved. |
| 66 def alert_fields(alerts_json): | 70 def alert_fields(alerts_json): |
| 67 filtered_json = {} | 71 filtered_json = {} |
| 68 for key, value in alerts_json.iteritems(): | 72 for key, value in alerts_json.iteritems(): |
| 69 if 'alerts' in key: | 73 if 'alerts' in key: |
| 70 filtered_json[key] = value | 74 filtered_json[key] = value |
| 71 return filtered_json | 75 return filtered_json |
| 72 | 76 |
| 73 if alert_fields(last_alerts) != alert_fields(alerts): | 77 if alert_fields(last_alerts) != alert_fields(alerts): |
| 74 new_entry = AlertsJSON(json=self.generate_json_dump(alerts)) | 78 new_entry = AlertsJSON( |
| 79 json=self.generate_json_dump(alerts), |
| 80 type=alerts_type) |
| 75 new_entry.put() | 81 new_entry.put() |
| 76 | 82 |
| 77 # Has no 'response' member. | 83 # Has no 'response' member. |
| 78 # pylint: disable=E1101 | 84 # pylint: disable=E1101 |
| 79 def post_to_memcache(self, memcache_key, alerts): | 85 def post_to_memcache(self, memcache_key, alerts): |
| 80 uncompressed = self.generate_json_dump(alerts) | 86 uncompressed = self.generate_json_dump(alerts) |
| 81 compression_level = 1 | 87 compression_level = 1 |
| 82 compressed = zlib.compress(uncompressed, compression_level) | 88 compressed = zlib.compress(uncompressed, compression_level) |
| 83 memcache.set(memcache_key, compressed) | 89 memcache.set(memcache_key, compressed) |
| 84 | 90 |
| 85 def parse_alerts(self, alerts_json): | 91 def parse_alerts(self, alerts_json): |
| 86 try: | 92 try: |
| 87 alerts = json.loads(alerts_json) | 93 alerts = json.loads(alerts_json) |
| 88 except ValueError: | 94 except ValueError: |
| 89 warning = 'content field was not JSON' | 95 warning = 'content field was not JSON' |
| 90 self.response.set_status(400, warning) | 96 self.response.set_status(400, warning) |
| 91 LOGGER.warn(warning) | 97 LOGGER.warn(warning) |
| 92 return | 98 return |
| 93 | 99 |
| 94 alerts.update({'date': datetime.datetime.utcnow()}) | 100 alerts.update({'date': datetime.datetime.utcnow()}) |
| 95 | 101 |
| 96 return alerts | 102 return alerts |
| 97 | 103 |
| 98 def update_alerts(self, memcache_key): | 104 def update_alerts(self, alerts_type): |
| 99 alerts = self.parse_alerts(self.request.get('content')) | 105 alerts = self.parse_alerts(self.request.get('content')) |
| 100 if alerts: | 106 if alerts: |
| 101 self.post_to_memcache(memcache_key, alerts) | 107 self.post_to_memcache(alerts_type, alerts) |
| 102 self.save_alerts_to_history(alerts) | 108 self.post_to_history(alerts_type, alerts) |
| 103 | 109 |
| 104 def post(self): | 110 def post(self): |
| 105 self.update_alerts(AlertsHandler.MEMCACHE_ALERTS_KEY) | 111 self.update_alerts(AlertsHandler.ALERTS_TYPE) |
| 112 |
| 113 |
| 114 class AlertsHistory(webapp2.RequestHandler): |
| 115 MAX_LIMIT_PER_PAGE = 50 |
| 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)) |
| 106 | 152 |
| 107 | 153 |
| 108 app = webapp2.WSGIApplication([ | 154 app = webapp2.WSGIApplication([ |
| 109 ('/alerts', AlertsHandler) | 155 ('/alerts', AlertsHandler), |
| 156 ('/alerts-history', AlertsHistory) |
| 110 ]) | 157 ]) |
| OLD | NEW |