OLD | NEW |
1 # coding=utf-8 | 1 # coding=utf-8 |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Status management pages.""" | 6 """Status management pages.""" |
7 | 7 |
8 import datetime | 8 import datetime |
9 import json | 9 import json |
10 import re | 10 import re |
11 | 11 |
12 from google.appengine.api import memcache | 12 from google.appengine.api import memcache |
13 from google.appengine.ext import db | 13 from google.appengine.ext import db |
14 | 14 |
15 from base_page import BasePage | 15 from base_page import BasePage |
16 import utils | 16 import utils |
17 | 17 |
18 | 18 |
19 ALLOWED_ORIGINS = [ | 19 ALLOWED_ORIGINS = [ |
20 'https://gerrit-int.chromium.org', | 20 'https://gerrit-int.chromium.org', |
21 'https://gerrit.chromium.org', | 21 'https://gerrit.chromium.org', |
22 'https://chrome-internal-review.googlesource.com', | 22 'https://chrome-internal-review.googlesource.com', |
23 'https://chromium-review.googlesource.com', | 23 'https://chromium-review.googlesource.com', |
24 ] | 24 ] |
25 | 25 |
26 | 26 |
| 27 class TextFragment(object): |
| 28 """Simple object to hold text that might be linked""" |
| 29 |
| 30 def __init__(self, text, target=None, is_email=False): |
| 31 self.text = text |
| 32 self.target = target |
| 33 self.is_email = is_email |
| 34 |
| 35 def __repr__(self): |
| 36 return 'TextFragment({%s->%s})' % (self.text, self.target) |
| 37 |
| 38 |
| 39 class LinkableText(object): |
| 40 """Turns arbitrary text into a set of links""" |
| 41 |
| 42 GERRIT_URLS = { |
| 43 'chrome': 'https://chrome-internal-review.googlesource.com', |
| 44 'chromium': 'https://chromium-review.googlesource.com', |
| 45 } |
| 46 |
| 47 WATERFALL_URLS = { |
| 48 'chromeos': 'http://chromegw/i/chromeos', |
| 49 'chromiumos': 'http://build.chromium.org/p/chromiumos', |
| 50 } |
| 51 |
| 52 # Automatically linkify known strings for the user. |
| 53 _CONVERTS = [] |
| 54 |
| 55 @classmethod |
| 56 def register_converter(cls, regex, target, pretty, is_email, flags=re.I): |
| 57 """Register a new conversion for creating links from text""" |
| 58 cls._CONVERTS.append( |
| 59 (re.compile(regex, flags=flags), target, pretty, is_email)) |
| 60 |
| 61 @classmethod |
| 62 def bootstrap(cls, _app_name): |
| 63 """Add conversions (possibly specific to |app_name| instance)""" |
| 64 # Convert CrOS bug links. Support the forms: |
| 65 # http://crbug.com/1234 |
| 66 # http://crosbug.com/1234 |
| 67 # crbug/1234 |
| 68 # crosbug/p/1234 |
| 69 cls.register_converter( |
| 70 # 1 2 3 4 5 6 7 |
| 71 r'\b((http://)?((crbug|crosbug)(\.com)?(/(p/)?[0-9]+)))\b', |
| 72 r'http://\4.com\6', r'\1', False) |
| 73 |
| 74 # Convert e-mail addresses. |
| 75 cls.register_converter( |
| 76 r'(([-+.a-z0-9_!#$%&*/=?^_`{|}~]+)@[-a-z0-9.]+\.[a-z0-9]+)\b', |
| 77 r'\1', r'\2', True) |
| 78 |
| 79 # Convert SHA1's to gerrit links. Assume all external since |
| 80 # there is no sane way to detect it's an internal CL. |
| 81 cls.register_converter( |
| 82 r'\b([0-9a-f]{40})\b', |
| 83 r'%s/#q,\1,n,z' % cls.GERRIT_URLS['chromium'], r'\1', False) |
| 84 |
| 85 # Convert public gerrit CL numbers which take the form: |
| 86 # CL:1234 |
| 87 cls.register_converter( |
| 88 r'\b(CL:([0-9]+))\b', |
| 89 r'%s/\2' % cls.GERRIT_URLS['chromium'], r'\1', False) |
| 90 # Convert internal gerrit CL numbers which take the form: |
| 91 # CL:*1234 |
| 92 cls.register_converter( |
| 93 r'\b(CL:\*([0-9]+))\b', |
| 94 r'%s/\2' % cls.GERRIT_URLS['chrome'], r'\1', False) |
| 95 |
| 96 # Match the string: |
| 97 # Automatic: "cbuildbot" on "x86-generic ASAN" from. |
| 98 # Do this for everyone since "cbuildbot" is unique to CrOS. |
| 99 # Otherwise, we'd do it only for chromium |app_name| instances. |
| 100 cls.register_converter( |
| 101 r'("cbuildbot" on "([^"]+ canary)")', |
| 102 r'%s/builders/\2' % cls.WATERFALL_URLS['chromeos'], r'\1', False) |
| 103 cls.register_converter( |
| 104 r'("cbuildbot" on "([^"]+)")', |
| 105 r'%s/builders/\2' % cls.WATERFALL_URLS['chromiumos'], r'\1', False) |
| 106 |
| 107 @classmethod |
| 108 def parse(cls, text): |
| 109 """Creates a list of TextFragment objects based on |text|""" |
| 110 if not text: |
| 111 return [] |
| 112 for prog, target, pretty_text, is_email in cls._CONVERTS: |
| 113 m = prog.search(text) |
| 114 if m: |
| 115 link = TextFragment(m.expand(pretty_text), |
| 116 target=m.expand(target), |
| 117 is_email=is_email) |
| 118 left_links = cls.parse(text[:m.start()].rstrip()) |
| 119 right_links = cls.parse(text[m.end():].lstrip()) |
| 120 return left_links + [link] + right_links |
| 121 return [TextFragment(text)] |
| 122 |
| 123 def __init__(self, text): |
| 124 self.raw_text = text |
| 125 self.links = self.parse(text.strip()) |
| 126 |
| 127 def __str__(self): |
| 128 return self.raw_text |
| 129 |
| 130 |
27 class Status(db.Model): | 131 class Status(db.Model): |
28 """Description for the status table.""" | 132 """Description for the status table.""" |
29 # The username who added this status. | 133 # The username who added this status. |
30 username = db.StringProperty(required=True) | 134 username = db.StringProperty(required=True) |
31 # The date when the status got added. | 135 # The date when the status got added. |
32 date = db.DateTimeProperty(auto_now_add=True) | 136 date = db.DateTimeProperty(auto_now_add=True) |
33 # The message. It can contain html code. | 137 # The message. It can contain html code. |
34 message = db.StringProperty(required=True) | 138 message = db.StringProperty(required=True) |
35 | 139 |
36 @property | 140 @property |
| 141 def username_links(self): |
| 142 return LinkableText(self.username) |
| 143 |
| 144 @property |
| 145 def message_links(self): |
| 146 return LinkableText(self.message) |
| 147 |
| 148 @property |
37 def general_state(self): | 149 def general_state(self): |
38 """Returns a string representing the state that the status message | 150 """Returns a string representing the state that the status message |
39 describes. | 151 describes. |
40 """ | 152 """ |
41 message = self.message | 153 message = self.message |
42 closed = re.search('close', message, re.IGNORECASE) | 154 closed = re.search('close', message, re.IGNORECASE) |
43 if closed and re.search('maint', message, re.IGNORECASE): | 155 if closed and re.search('maint', message, re.IGNORECASE): |
44 return 'maintenance' | 156 return 'maintenance' |
45 if re.search('throt', message, re.IGNORECASE): | 157 if re.search('throt', message, re.IGNORECASE): |
46 return 'throttled' | 158 return 'throttled' |
(...skipping 244 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
291 return self._handle(error_message, last_message) | 403 return self._handle(error_message, last_message) |
292 else: | 404 else: |
293 put_status(Status(message=new_message, username=self.user.email())) | 405 put_status(Status(message=new_message, username=self.user.email())) |
294 self.redirect("/") | 406 self.redirect("/") |
295 | 407 |
296 | 408 |
297 def bootstrap(): | 409 def bootstrap(): |
298 # Guarantee that at least one instance exists. | 410 # Guarantee that at least one instance exists. |
299 if db.GqlQuery('SELECT __key__ FROM Status').get() is None: | 411 if db.GqlQuery('SELECT __key__ FROM Status').get() is None: |
300 Status(username='none', message='welcome to status').put() | 412 Status(username='none', message='welcome to status').put() |
| 413 LinkableText.bootstrap(BasePage.APP_NAME) |
OLD | NEW |