1 # Copyright (c) 2012 The Chromium OS 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.
11 from google.appengine.api import datastore_errors
12 from google.appengine.ext import db
13 from google.appengine.api import users
20 # Could replace this with a function if there is ever any reason
21 # to spread entries over multiple datastores. Consistency is only
22 # gauranteed within a datastore, but access should be limited to
23 # about 1 per second. That should not be a problem for us.
24 DATASTORE_KEY = db.Key.from_path('Stats', 'default')
26 JINJA_ENVIRONMENT = jinja2.Environment(
27 loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
28 extensions=['jinja2.ext.autoescape'],
32 class MainPage(webapp2.RequestHandler):
33 """Provide interface for interacting with DB."""
35 # Regex to peel SQL-like SELECT off front, if present, grabbing SELECT args.
36 # Example: "SELECT foo,bar WHERE blah blah"
37 # ==> group(1)="foo,bar", group(2)="WHERE blah blah"
38 # Example: "SELECT foo , bar"
39 # ==> group(1)="foo , bar", group(2)=""
40 # Example: "WHERE blah blah"
42 QUERY_SELECT_PREFIX_RE = re.compile(r'^\s*SELECT\s+'
43 r'([^\s,]+(?:\s*,\s*[^\s,]+)*)' # Group 1
44 r'(?:$|\s+)(.*)', # Group 2
45 re.IGNORECASE | re.VERBOSE)
47 # Regex to determine if WHERE is present, and capture everything after it.
48 # Example: "WHERE foo=bar ORDER BY whatever"
49 # ==> group(1)="foo=bar ORDER BY whatever"
50 # Example: "ORDER BY whatever"
52 QUERY_WHERE_PREFIX_RE = re.compile(r'^WHERE\s+(.+)$',
53 re.IGNORECASE | re.VERBOSE)
55 # Regex to discover ORDER BY columns in order to highlight them in results.
56 QUERY_ORDER_RE = re.compile(r'ORDER\s+BY\s+(\S+)', re.IGNORECASE)
58 # Regex to discover LIMIT value in query.
59 QUERY_LIMIT_RE = re.compile(r'LIMIT\s+(\d+)', re.IGNORECASE)
61 # Regex for separating tokens by commas, allowing spaces on either side.
62 COMMA_RE = re.compile(r'\s*,\s*')
64 # Default columns to show in results table if no SELECT given.
65 DEFAULT_COLUMNS = ['end_date', 'cmd_line', 'run_time', 'board',
68 # All possible columns in Statistics model.
69 ALL_COLUMNS = sorted(model.Statistics.properties())
71 # Provide example queries in interface as a form of documentation.
73 ("ORDER BY end_date,run_time"
75 ("WHERE username='mtennant'"
76 " ORDER BY end_date DESC"
78 ("SELECT end_datetime,cmd_base,cmd_args,run_time,package_count"
79 " WHERE board='amd64-generic'"
80 " ORDER BY end_datetime"
82 ("SELECT end_date,cmd_base,run_time,board,package_count"
83 " WHERE end_date=DATE('2012-03-28')"
86 ("SELECT end_date,cmd_base,cmd_args,run_time,username"
92 """Support GET to stats page."""
93 # Note that google.com authorization is required to access this page, which
94 # is controlled in app.yaml and on appspot admin page.
95 orig_query = self.request.get('query')
96 logging.debug('Received raw query %r', orig_query)
98 # If no LIMIT was provided, default to a LIMIT of 30 for sanity.
99 if not self.QUERY_LIMIT_RE.search(orig_query):
100 orig_query += ' LIMIT 30'
104 # Peel off "SELECT" clause from front of query. GCL does not support SELECT
105 # filtering, but we will support it right here to select/filter columns.
106 query, columns = self._RemoveSelectFromQuery(query)
107 if query == orig_query and columns == self.DEFAULT_COLUMNS:
108 # This means there was no SELECT in query. That is equivalent to
109 # SELECT of default columns, so show that to user.
110 orig_query = 'SELECT %s %s' % (','.join(columns), orig_query)
112 # All queries should have the "ancestor" WHERE clause in them, but that
113 # need not be exposed to interface. Insert the clause intelligently.
114 query = self._AdjustWhereInQuery(query)
119 stat_entries = model.Statistics.gql(query, DATASTORE_KEY)
120 except datastore_errors.BadQueryError as ex:
121 error_msg = '<p>%s.</p><p>Actual GCL query used: "%s"</p>' % (ex, query)
123 if self.request.get('format') == 'json':
124 # Write output in the JSON format.
125 d = self._ResultsToDictionary(stat_entries, columns)
127 class CustomEncoder(json.JSONEncoder):
128 """Handles non-serializable classes by converting them to strings."""
129 def default(self, obj):
130 if (isinstance(obj, datetime.datetime) or
131 isinstance(obj, datetime.date) or
132 isinstance(obj, datetime.time)):
133 return obj.isoformat()
135 return json.JSONEncoder.default(self, obj)
137 self.response.content_type = 'application/json'
138 self.response.write(json.dumps(d, cls=CustomEncoder))
140 # Write output to the HTML page.
141 results_table = self._PrepareResultsTable(stat_entries, columns)
143 'error_msg': error_msg,
145 'user_query': orig_query,
146 'user_email': users.get_current_user(),
147 'results_table': results_table,
148 'column_list': self.ALL_COLUMNS,
149 'example_queries': self.EXAMPLE_QUERIES,
151 template = JINJA_ENVIRONMENT.get_template('index.html')
152 self.response.write(template.render(template_values))
154 def _RemoveSelectFromQuery(self, query):
155 """Remove SELECT clause from |query|, return tuple (new_query, columns)."""
156 match = self.QUERY_SELECT_PREFIX_RE.search(query)
158 # A SELECT clause is present. Remove it but save requested columns.
159 columns = self.COMMA_RE.split(match.group(1))
160 query = match.group(2)
163 columns = self.ALL_COLUMNS
165 logging.debug('Columns selected for viewing: %s', ', '.join(columns))
166 return query, columns
168 logging.debug('Using default columns for viewing: %s',
169 ', '.join(self.DEFAULT_COLUMNS))
170 return query, self.DEFAULT_COLUMNS
172 def _AdjustWhereInQuery(self, query):
173 """Insert WHERE ANCESTOR into |query| and return."""
174 match = self.QUERY_WHERE_PREFIX_RE.search(query)
176 return 'WHERE ANCESTOR IS :1 AND %s' % match.group(1)
178 return 'WHERE ANCESTOR IS :1 %s' % query
180 def _PrepareResultsTable(self, stat_entries, columns):
181 """Prepare table for |stat_entries| using only |columns|."""
182 # One header blank for row numbers, then each column name.
183 table = [[c for c in [''] + columns]]
184 # Prepare list of table rows, one for each stat entry.
185 for stat_ix, stat_entry in enumerate(stat_entries):
187 row += [getattr(stat_entry, col) for col in columns]
192 def _ResultsToDictionary(self, stat_entries, columns):
193 """Converts |stat_entries| to a dictionary with |columns| as keys.
196 stat_entries: A list of GqlQuery objects.
197 columns: A list of keys to use.
200 A dictionary with |columns| as keys.
203 keys = [c for c in columns]
204 for stat_ix, stat_entry in enumerate(stat_entries):
205 stats_dict[stat_ix] = dict(
206 (col, getattr(stat_entry, col)) for col in columns)
211 class PostPage(webapp2.RequestHandler):
212 """Provides interface for uploading command stats to database."""
214 NO_VALUE = '__NO_VALUE_AT_ALL__'
217 """Support POST of command stats."""
218 logging.info('Stats POST received at %r', self.request.uri)
220 new_stat = model.Statistics(parent=DATASTORE_KEY)
222 # Check each supported DB property to see if it has a value set
223 # in the POST request.
224 for prop in model.Statistics.properties():
225 # Skip properties with auto_now or auto_now_add enabled.
226 model_prop = getattr(model.Statistics, prop)
227 if ((hasattr(model_prop, 'auto_now_add') and model_prop.auto_now_add) or
228 (hasattr(model_prop, 'auto_now') and model_prop.auto_now)):
231 # Note that using hasattr with self.request does not work at all.
232 # It (almost) always says the attribute is not present, when getattr
233 # does actually return a value. Also note that self.request.get is
234 # not returning None as the default value if no explicit default value
235 # is provided, contrary to the spec for dict.get.
236 value = self.request.get(prop, self.NO_VALUE)
238 if value is not self.NO_VALUE:
239 # String properties must be 500 characters or less (GQL requirement).
240 if isinstance(model_prop, db.StringProperty) and len(value) > 500:
241 logging.debug(' String property %r too long. Cutting off at 500'
242 ' characters.', prop)
245 # Integer properties require casting
246 if isinstance(model_prop, db.IntegerProperty):
249 logging.debug(' Stats POST property %r ==> %r', prop, value)
250 setattr(new_stat, prop, value)
252 # Use automatically set end_datetime prop to set end_date and end_time.
253 new_stat.end_time = new_stat.end_datetime.time()
254 new_stat.end_date = new_stat.end_datetime.date()