| Index: appengine_apps/chromium_status/appengine_module/chromium_status/status.py
|
| diff --git a/appengine_apps/chromium_status/appengine_module/chromium_status/status.py b/appengine_apps/chromium_status/appengine_module/chromium_status/status.py
|
| deleted file mode 100644
|
| index 6c58e8bdec0b371a1696bc524742536491d4b40b..0000000000000000000000000000000000000000
|
| --- a/appengine_apps/chromium_status/appengine_module/chromium_status/status.py
|
| +++ /dev/null
|
| @@ -1,444 +0,0 @@
|
| -# coding=utf-8
|
| -# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -"""Status management pages."""
|
| -
|
| -import datetime
|
| -import json
|
| -import re
|
| -
|
| -from google.appengine.api import memcache
|
| -from google.appengine.ext import db
|
| -
|
| -from appengine_module.chromium_status.base_page import BasePage
|
| -from appengine_module.chromium_status import utils
|
| -
|
| -
|
| -ALLOWED_ORIGINS = [
|
| - 'https://gerrit-int.chromium.org',
|
| - 'https://gerrit.chromium.org',
|
| - 'https://chrome-internal-review.googlesource.com',
|
| - 'https://chromium-review.googlesource.com',
|
| -]
|
| -
|
| -
|
| -class TextFragment(object):
|
| - """Simple object to hold text that might be linked"""
|
| -
|
| - def __init__(self, text, target=None, is_email=False):
|
| - self.text = text
|
| - self.target = target
|
| - self.is_email = is_email
|
| -
|
| - def __repr__(self):
|
| - return 'TextFragment({%s->%s})' % (self.text, self.target)
|
| -
|
| -
|
| -class LinkableText(object):
|
| - """Turns arbitrary text into a set of links"""
|
| -
|
| - GERRIT_URLS = {
|
| - 'chrome': 'https://chrome-internal-review.googlesource.com',
|
| - 'chromium': 'https://chromium-review.googlesource.com',
|
| - }
|
| -
|
| - WATERFALL_URLS = {
|
| - 'chromeos': 'https://uberchromegw.corp.google.com/i/chromeos',
|
| - 'chromiumos': 'http://build.chromium.org/p/chromiumos',
|
| - }
|
| -
|
| - APP_PREFIXES = (
|
| - 'dev~',
|
| - 's~',
|
| - )
|
| -
|
| - # Automatically linkify known strings for the user.
|
| - _CONVERTS = []
|
| -
|
| - @classmethod
|
| - def register_converter(cls, regex, target, pretty, is_email, flags=re.I):
|
| - """Register a new conversion for creating links from text"""
|
| - cls._CONVERTS.append(
|
| - (re.compile(regex, flags=flags), target, pretty, is_email))
|
| -
|
| - @classmethod
|
| - def bootstrap(cls, is_chromiumos):
|
| - """Add conversions (possibly specific to |app_name| instance)"""
|
| - # Convert CrOS bug links. Support the forms:
|
| - # http://crbug.com/1234
|
| - # http://crosbug.com/1234
|
| - # crbug/1234
|
| - # crosbug/p/1234
|
| - cls.register_converter(
|
| - # 1 2 3 4 5 6 7
|
| - r'\b((http://)?((crbug|crosbug)(\.com)?(/(p/)?[0-9]+)))\b',
|
| - r'http://\4.com\6', r'\1', False)
|
| -
|
| - # Convert e-mail addresses.
|
| - cls.register_converter(
|
| - r'(([-+.a-z0-9_!#$%&*/=?^_`{|}~]+)@[-a-z0-9.]+\.[a-z0-9]+)\b',
|
| - r'\1', r'\2', True)
|
| -
|
| - # Convert SHA1's to gerrit links. Assume all external since
|
| - # there is no sane way to detect it's an internal CL.
|
| - cls.register_converter(
|
| - r'\b([0-9a-f]{40})\b',
|
| - r'%s/#q,\1,n,z' % cls.GERRIT_URLS['chromium'], r'\1', False)
|
| -
|
| - # Convert public gerrit CL numbers which take the form:
|
| - # CL:1234
|
| - cls.register_converter(
|
| - r'\b(CL:([0-9]+))\b',
|
| - r'%s/\2' % cls.GERRIT_URLS['chromium'], r'\1', False)
|
| - # Convert internal gerrit CL numbers which take the form:
|
| - # CL:*1234
|
| - cls.register_converter(
|
| - r'\b(CL:\*([0-9]+))\b',
|
| - r'%s/\2' % cls.GERRIT_URLS['chrome'], r'\1', False)
|
| -
|
| - # Match the string:
|
| - # Automatic: "cbuildbot" on "x86-generic ASAN" from.
|
| - # Do this for everyone since "cbuildbot" is unique to CrOS.
|
| - # Otherwise, we'd do it only for chromium |app_name| instances.
|
| - cls.register_converter(
|
| - r'("cbuildbot" on "([^"]+ (canary|master|launcher))")',
|
| - r'%s/builders/\2' % cls.WATERFALL_URLS['chromeos'], r'\1', False)
|
| - cls.register_converter(
|
| - r'("cbuildbot" on "([^"]+)")',
|
| - r'%s/builders/\2' % cls.WATERFALL_URLS['chromiumos'], r'\1', False)
|
| -
|
| - if is_chromiumos:
|
| - # Match the string '"builder name"-internal/public-buildnumber:'. E.g.,
|
| - # "Canary master"-i-120:
|
| - # This applies only to the CrOS instance where the builders may update
|
| - # the tree status directly.
|
| - cls.register_converter(
|
| - r'("([\w\s]+)"-i-(\d+):)',
|
| - r'%s/builders/\2/builds/\3' % cls.WATERFALL_URLS['chromeos'],
|
| - r'\1', False
|
| - )
|
| - cls.register_converter(
|
| - r'("([\w\s]+)"-p-(\d+):)',
|
| - r'%s/builders/\2/builds/\3' % cls.WATERFALL_URLS['chromiumos'],
|
| - r'\1', False
|
| - )
|
| -
|
| - @classmethod
|
| - def parse(cls, text):
|
| - """Creates a list of TextFragment objects based on |text|"""
|
| - if not text:
|
| - return []
|
| - for prog, target, pretty_text, is_email in cls._CONVERTS:
|
| - m = prog.search(text)
|
| - if m:
|
| - link = TextFragment(m.expand(pretty_text),
|
| - target=m.expand(target),
|
| - is_email=is_email)
|
| - left_links = cls.parse(text[:m.start()].rstrip())
|
| - right_links = cls.parse(text[m.end():].lstrip())
|
| - return left_links + [link] + right_links
|
| - return [TextFragment(text)]
|
| -
|
| - def __init__(self, text):
|
| - self.raw_text = text
|
| - self.links = self.parse(text.strip())
|
| -
|
| - def __str__(self):
|
| - return self.raw_text
|
| -
|
| -
|
| -class Status(db.Model):
|
| - """Description for the status table."""
|
| - # The username who added this status.
|
| - username = db.StringProperty(required=True)
|
| - # The date when the status got added.
|
| - date = db.DateTimeProperty(auto_now_add=True)
|
| - # The message. It can contain html code.
|
| - message = db.StringProperty(required=True)
|
| -
|
| - def __init__(self, *args, **kwargs):
|
| - # Normalize newlines otherwise the DB store barfs. We don't really want to
|
| - # make this field handle newlines as none of the places where we output the
|
| - # content is designed to handle it, nor the clients that consume us.
|
| - kwargs['message'] = kwargs.get('message', '').replace('\n', ' ')
|
| - super(Status, self).__init__(*args, **kwargs)
|
| -
|
| - @property
|
| - def username_links(self):
|
| - return LinkableText(self.username)
|
| -
|
| - @property
|
| - def message_links(self):
|
| - return LinkableText(self.message)
|
| -
|
| - @property
|
| - def general_state(self):
|
| - """Returns a string representing the state that the status message
|
| - describes.
|
| -
|
| - Note: Keep in sync with main.html help text.
|
| - """
|
| - message = self.message
|
| - closed = re.search('close', message, re.IGNORECASE)
|
| - if closed and re.search('maint', message, re.IGNORECASE):
|
| - return 'maintenance'
|
| - if re.search('throt', message, re.IGNORECASE):
|
| - return 'throttled'
|
| - if closed:
|
| - return 'closed'
|
| - return 'open'
|
| -
|
| - @property
|
| - def can_commit_freely(self):
|
| - return self.general_state == 'open'
|
| -
|
| - def AsDict(self):
|
| - data = super(Status, self).AsDict()
|
| - data['general_state'] = self.general_state
|
| - data['can_commit_freely'] = self.can_commit_freely
|
| - return data
|
| -
|
| -
|
| -def get_status():
|
| - """Returns the current Status, e.g. the most recent one."""
|
| - status = memcache.get('last_status')
|
| - if status is None:
|
| - status = Status.all().order('-date').get()
|
| - # Use add instead of set(); must not change it if it was already set.
|
| - memcache.add('last_status', status)
|
| - return status
|
| -
|
| -
|
| -def put_status(status):
|
| - """Sets the current Status, e.g. append a new one."""
|
| - status.put()
|
| - memcache.set('last_status', status)
|
| - memcache.delete('last_statuses')
|
| -
|
| -
|
| -def get_last_statuses(limit):
|
| - """Returns the last |limit| statuses."""
|
| - statuses = memcache.get('last_statuses')
|
| - if not statuses or len(statuses) < limit:
|
| - statuses = Status.all().order('-date').fetch(limit)
|
| - memcache.add('last_statuses', statuses)
|
| - return statuses[:limit]
|
| -
|
| -
|
| -def parse_date(date):
|
| - """Parses a date."""
|
| - match = re.match(r'^(\d\d\d\d)-(\d\d)-(\d\d)$', date)
|
| - if match:
|
| - return datetime.datetime(
|
| - int(match.group(1)), int(match.group(2)), int(match.group(3)))
|
| - if date.isdigit():
|
| - return datetime.datetime.utcfromtimestamp(int(date))
|
| - return None
|
| -
|
| -
|
| -def limit_length(string, length):
|
| - """Limits the string |string| at length |length|.
|
| -
|
| - Inserts an ellipsis if it is cut.
|
| - """
|
| - string = unicode(string.strip())
|
| - if len(string) > length:
|
| - string = string[:length - 1] + u'…'
|
| - return string
|
| -
|
| -
|
| -class AllStatusPage(BasePage):
|
| - """Displays a big chunk, 1500, status values."""
|
| - @utils.requires_read_access
|
| - def get(self):
|
| - query = db.Query(Status).order('-date')
|
| - start_date = self.request.get('startTime')
|
| - if start_date:
|
| - query.filter('date <', parse_date(start_date))
|
| - try:
|
| - limit = int(self.request.get('limit'))
|
| - except ValueError:
|
| - limit = 1000
|
| - end_date = self.request.get('endTime')
|
| - beyond_end_of_range_status = None
|
| - if end_date:
|
| - query.filter('date >=', parse_date(end_date))
|
| - # We also need to get the very next status in the range, otherwise
|
| - # the caller can't tell what the effective tree status was at time
|
| - # |end_date|.
|
| - beyond_end_of_range_status = Status.all(
|
| - ).filter('date <', end_date).order('-date').get()
|
| -
|
| - out_format = self.request.get('format', 'csv')
|
| - if out_format == 'csv':
|
| - # It's not really an html page.
|
| - self.response.headers['Content-Type'] = 'text/plain'
|
| - template_values = self.InitializeTemplate(self.APP_NAME + ' Tree Status')
|
| - template_values['status'] = query.fetch(limit)
|
| - template_values['beyond_end_of_range_status'] = beyond_end_of_range_status
|
| - self.DisplayTemplate('allstatus.html', template_values)
|
| - elif out_format == 'json':
|
| - self.response.headers['Content-Type'] = 'application/json'
|
| - self.response.headers['Access-Control-Allow-Origin'] = '*'
|
| - statuses = [s.AsDict() for s in query.fetch(limit)]
|
| - if beyond_end_of_range_status:
|
| - statuses.append(beyond_end_of_range_status.AsDict())
|
| - data = json.dumps(statuses)
|
| - callback = self.request.get('callback')
|
| - if callback:
|
| - if re.match(r'^[a-zA-Z$_][a-zA-Z$0-9._]*$', callback):
|
| - data = '%s(%s);' % (callback, data)
|
| - self.response.out.write(data)
|
| - else:
|
| - self.response.headers['Content-Type'] = 'text/plain'
|
| - self.response.out.write('Invalid format')
|
| -
|
| -
|
| -class CurrentPage(BasePage):
|
| - """Displays the /current page."""
|
| -
|
| - def get(self):
|
| - # Show login link on current status bar when login is required.
|
| - out_format = self.request.get('format', 'html')
|
| - if out_format == 'html' and not self.read_access and not self.user:
|
| - template_values = self.InitializeTemplate(self.APP_NAME + ' Tree Status')
|
| - template_values['show_login'] = True
|
| - self.DisplayTemplate('current.html', template_values, use_cache=True)
|
| - else:
|
| - self._handle()
|
| -
|
| - @utils.requires_read_access
|
| - def _handle(self):
|
| - """Displays the current message in various formats."""
|
| - out_format = self.request.get('format', 'html')
|
| - status = get_status()
|
| - if out_format == 'raw':
|
| - self.response.headers['Content-Type'] = 'text/plain'
|
| - self.response.headers['Access-Control-Allow-Origin'] = '*'
|
| - self.response.out.write(status.message)
|
| - elif out_format == 'json':
|
| - self.response.headers['Content-Type'] = 'application/json'
|
| - origin = self.request.headers.get('Origin')
|
| - if self.request.get('with_credentials') and origin in ALLOWED_ORIGINS:
|
| - self.response.headers['Access-Control-Allow-Origin'] = origin
|
| - self.response.headers['Access-Control-Allow-Credentials'] = 'true'
|
| - else:
|
| - self.response.headers['Access-Control-Allow-Origin'] = '*'
|
| - data = json.dumps(status.AsDict())
|
| - callback = self.request.get('callback')
|
| - if callback:
|
| - if re.match(r'^[a-zA-Z$_][a-zA-Z$0-9._]*$', callback):
|
| - data = '%s(%s);' % (callback, data)
|
| - self.response.out.write(data)
|
| - elif out_format == 'html':
|
| - template_values = self.InitializeTemplate(self.APP_NAME + ' Tree Status')
|
| - template_values['show_login'] = False
|
| - template_values['message'] = status.message
|
| - template_values['state'] = status.general_state
|
| - self.DisplayTemplate('current.html', template_values, use_cache=True)
|
| - else:
|
| - self.error(400)
|
| -
|
| -
|
| -class StatusPage(BasePage):
|
| - """Displays the /status page."""
|
| -
|
| - def get(self):
|
| - """Displays 1 if the tree is open, and 0 if the tree is closed."""
|
| - # NOTE: This item is always public to allow waterfalls to check it.
|
| - status = get_status()
|
| - self.response.headers['Cache-Control'] = 'no-cache, private, max-age=0'
|
| - self.response.headers['Content-Type'] = 'text/plain'
|
| - self.response.out.write(str(int(status.can_commit_freely)))
|
| -
|
| - @utils.requires_bot_login
|
| - @utils.requires_write_access
|
| - def post(self):
|
| - """Adds a new message from a backdoor.
|
| -
|
| - The main difference with MainPage.post() is that it doesn't look for
|
| - conflicts and doesn't redirect to /.
|
| - """
|
| - message = self.request.get('message')
|
| - message = limit_length(message, 500)
|
| - username = self.request.get('username')
|
| - if message and username:
|
| - put_status(Status(message=message, username=username))
|
| - self.response.out.write('OK')
|
| -
|
| -
|
| -class StatusViewerPage(BasePage):
|
| - """Displays the /status_viewer page."""
|
| -
|
| - @utils.requires_read_access
|
| - def get(self):
|
| - """Displays status_viewer.html template."""
|
| - template_values = self.InitializeTemplate(self.APP_NAME + ' Tree Status')
|
| - self.DisplayTemplate('status_viewer.html', template_values)
|
| -
|
| -
|
| -class MainPage(BasePage):
|
| - """Displays the main page containing the last 25 messages."""
|
| -
|
| - # NOTE: This is require_login in order to ensure that authentication doesn't
|
| - # happen while changing the tree status.
|
| - @utils.requires_login
|
| - @utils.requires_read_access
|
| - def get(self):
|
| - return self._handle()
|
| -
|
| - def _handle(self, error_message='', last_message=''):
|
| - """Sets the information to be displayed on the main page."""
|
| - try:
|
| - limit = min(max(int(self.request.get('limit')), 1), 1000)
|
| - except ValueError:
|
| - limit = 25
|
| - status = get_last_statuses(limit)
|
| - current_status = get_status()
|
| - if not last_message:
|
| - last_message = current_status.message
|
| -
|
| - template_values = self.InitializeTemplate(self.APP_NAME + ' Tree Status')
|
| - template_values['status'] = status
|
| - template_values['message'] = last_message
|
| - template_values['last_status_key'] = current_status.key()
|
| - template_values['error_message'] = error_message
|
| - template_values['limit'] = limit
|
| - self.DisplayTemplate('main.html', template_values)
|
| -
|
| - @utils.requires_login
|
| - @utils.requires_write_access
|
| - def post(self):
|
| - """Adds a new message."""
|
| - # We pass these variables back into get(), prepare them.
|
| - last_message = ''
|
| - error_message = ''
|
| -
|
| - # Get the posted information.
|
| - new_message = self.request.get('message')
|
| - new_message = limit_length(new_message, 500)
|
| - last_status_key = self.request.get('last_status_key')
|
| - if not new_message:
|
| - # A submission contained no data. It's a better experience to redirect
|
| - # in this case.
|
| - self.redirect("/")
|
| - return
|
| -
|
| - current_status = get_status()
|
| - if current_status and (last_status_key != str(current_status.key())):
|
| - error_message = ('Message not saved, mid-air collision detected, '
|
| - 'please resolve any conflicts and try again!')
|
| - last_message = new_message
|
| - return self._handle(error_message, last_message)
|
| - else:
|
| - put_status(Status(message=new_message, username=self.user.email()))
|
| - self.redirect("/")
|
| -
|
| -
|
| -def bootstrap():
|
| - # Guarantee that at least one instance exists.
|
| - if db.GqlQuery('SELECT __key__ FROM Status').get() is None:
|
| - Status(username='none', message='welcome to status').put()
|
| - LinkableText.bootstrap(BasePage.IS_CHROMIUMOS)
|
|
|