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 ] | 22 ] |
23 | 23 |
24 | 24 |
25 class Link(object): | |
26 """Simple object to hold text that might be linked""" | |
M-A Ruel
2013/11/25 16:18:14
"""Holds text ..
No need to state a relative qual
vapier
2013/11/25 17:51:26
i agree it's simple, but i find scanning a one lin
| |
27 | |
28 def __init__(self, text, target=None, is_email=False): | |
M-A Ruel
2013/11/25 16:18:14
I'd prefer to not use default arguments.
vapier
2013/11/25 17:51:26
any reason why ? the intention is for email to be
| |
29 self.text = text | |
30 self.target = target | |
31 self.is_email = is_email | |
32 | |
33 def __repr__(self): | |
34 return 'Link({%s->%s})' % (self.text, self.target) | |
35 | |
36 | |
37 class LinkableText(object): | |
38 """Turn arbitrary text into a set of links""" | |
M-A Ruel
2013/11/25 16:18:14
Turns
(imperative everywhere)
vapier
2013/11/25 17:51:26
i dont see the point, but i also dont really care
| |
39 | |
40 GERRIT_URLS = { | |
41 'chrome': 'https://chrome-internal-review.googlesource.com', | |
M-A Ruel
2013/11/25 16:18:14
Very hardcoded. :/ Could these be entities?
vapier
2013/11/25 17:51:26
i have no idea what "entities" are. plus this fil
| |
42 'chromium': 'https://chromium-review.googlesource.com', | |
43 } | |
44 | |
45 WATERFALL_URLS = { | |
46 'chromeos': 'http://chromegw/i/chromeos', | |
47 'chromiumos': 'http://build.chromium.org/p/chromiumos', | |
48 } | |
49 | |
50 # Automatically linkify convert known strings for the user. | |
51 _CONVERTS = [ | |
52 # Convert CrOS bug links. Support the forms: | |
53 # http://crbug.com/1234 | |
54 # http://crosbug.com/1234 | |
55 # crbug/1234 | |
56 # crosbug/p/1234 | |
57 (re.compile( | |
58 # 1 2 3 4 5 6 7 | |
59 r'\b((http://)?((crbug|crosbug)(\.com)?(/(p/)?[0-9]+)))\b', | |
60 flags=re.I), | |
61 r'http://\4.com\6', r'\1', False), | |
62 | |
63 # Convert e-mail addresses. | |
64 (re.compile(r'(([-+.a-z0-9_!#$%&*/=?^_`{|}~]+)@[-a-z0-9.]+\.[a-z0-9]+)\b', | |
65 flags=re.I), | |
66 r'\1', r'\2', True), | |
67 | |
68 # Convert SHA1's to gerrit links. Assume all external since | |
69 # there is no sane way to detect it's an internal CL. | |
70 (re.compile(r'\b([0-9a-f]{40})\b', flags=re.I), | |
71 r'%s/#q,\1,n,z' % GERRIT_URLS['chromium'], r'\1', False), | |
72 | |
73 # Convert public gerrit CL numbers. | |
74 (re.compile(r'\b(CL:([0-9]+))\b', flags=re.I), | |
75 r'%s/\2' % GERRIT_URLS['chromium'], r'\1', False), | |
76 # Convert internal gerrit CL numbers. | |
77 (re.compile(r'\b(CL:\*([0-9]+))\b', flags=re.I), | |
78 r'%s/\2' % GERRIT_URLS['chrome'], r'\1', False), | |
79 ] | |
80 | |
81 @classmethod | |
82 def bootstrap(cls, _app_name): | |
83 """Add conversions specific to |app_name| instance""" | |
84 cls._CONVERTS += [ | |
85 # Do this for everyone since "cbuildbot" is unique to CrOS. | |
86 # Match the string: | |
87 # Automatic: "cbuildbot" on "x86-generic ASAN" from. | |
88 (re.compile(r'("cbuildbot" on "([^"]+ canary)")', | |
M-A Ruel
2013/11/25 16:18:14
These should be stated as static lists. There isn'
vapier
2013/11/25 17:51:26
this was needed in a previous CL where i used a fu
| |
89 flags=re.I), | |
90 r'%s/builders/\2' % cls.WATERFALL_URLS['chromeos'], r'\1', False), | |
91 (re.compile(r'("cbuildbot" on "([^"]+)")', | |
92 flags=re.I), | |
93 r'%s/builders/\2' % cls.WATERFALL_URLS['chromiumos'], r'\1', False), | |
94 ] | |
95 | |
96 @classmethod | |
97 def parse(cls, text): | |
98 """Create a list of Link objects based on |text|""" | |
99 if not text: | |
100 return [] | |
101 for prog, target, pretty_text, is_email in cls._CONVERTS: | |
102 m = prog.search(text) | |
103 if m: | |
104 link = Link(m.expand(pretty_text), | |
105 target=m.expand(target), | |
106 is_email=is_email) | |
107 left_links = cls.parse(text[:m.start()].rstrip()) | |
108 right_links = cls.parse(text[m.end():].lstrip()) | |
109 return left_links + [link] + right_links | |
110 return [Link(text)] | |
111 | |
112 def __init__(self, text): | |
113 self.raw_text = text | |
114 self.links = self.parse(text.strip()) | |
115 | |
116 def __str__(self): | |
117 return self.raw_text | |
118 | |
119 | |
25 class Status(db.Model): | 120 class Status(db.Model): |
26 """Description for the status table.""" | 121 """Description for the status table.""" |
27 # The username who added this status. | 122 # The username who added this status. |
28 username = db.StringProperty(required=True) | 123 username = db.StringProperty(required=True) |
29 # The date when the status got added. | 124 # The date when the status got added. |
30 date = db.DateTimeProperty(auto_now_add=True) | 125 date = db.DateTimeProperty(auto_now_add=True) |
31 # The message. It can contain html code. | 126 # The message. It can contain html code. |
32 message = db.StringProperty(required=True) | 127 message = db.StringProperty(required=True) |
33 | 128 |
129 def __init__(self, *args, **kwargs): | |
130 # Normalize newlines otherwise the DB store barfs. | |
M-A Ruel
2013/11/25 16:18:14
I'm fine with adding multiline=True instead. Actua
vapier
2013/11/25 17:51:26
i don't think we want people to insert newlines in
| |
131 kwargs['message'] = kwargs.get('message', '').replace('\n', '') | |
132 | |
133 super(Status, self).__init__(*args, **kwargs) | |
134 self.username_links = LinkableText(self.username) | |
135 self.message_links = LinkableText(self.message) | |
136 | |
34 @property | 137 @property |
35 def general_state(self): | 138 def general_state(self): |
36 """Returns a string representing the state that the status message | 139 """Returns a string representing the state that the status message |
37 describes. | 140 describes. |
38 """ | 141 """ |
39 message = self.message | 142 message = self.message |
40 closed = re.search('close', message, re.IGNORECASE) | 143 closed = re.search('close', message, re.IGNORECASE) |
41 if closed and re.search('maint', message, re.IGNORECASE): | 144 if closed and re.search('maint', message, re.IGNORECASE): |
42 return 'maintenance' | 145 return 'maintenance' |
43 if re.search('throt', message, re.IGNORECASE): | 146 if re.search('throt', message, re.IGNORECASE): |
(...skipping 195 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
239 | 342 |
240 # NOTE: This is require_login in order to ensure that authentication doesn't | 343 # NOTE: This is require_login in order to ensure that authentication doesn't |
241 # happen while changing the tree status. | 344 # happen while changing the tree status. |
242 @utils.requires_login | 345 @utils.requires_login |
243 @utils.requires_read_access | 346 @utils.requires_read_access |
244 def get(self): | 347 def get(self): |
245 return self._handle() | 348 return self._handle() |
246 | 349 |
247 def _handle(self, error_message='', last_message=''): | 350 def _handle(self, error_message='', last_message=''): |
248 """Sets the information to be displayed on the main page.""" | 351 """Sets the information to be displayed on the main page.""" |
352 LinkableText.bootstrap(self.APP_NAME) | |
M-A Ruel
2013/11/25 16:18:14
This is a bad idea to do this there. It creates a
vapier
2013/11/25 17:51:26
i'm not terribly familiar with the app engine mode
| |
353 | |
249 try: | 354 try: |
250 limit = min(max(int(self.request.get('limit')), 1), 1000) | 355 limit = min(max(int(self.request.get('limit')), 1), 1000) |
251 except ValueError: | 356 except ValueError: |
252 limit = 25 | 357 limit = 25 |
253 status = get_last_statuses(limit) | 358 status = get_last_statuses(limit) |
254 current_status = get_status() | 359 current_status = get_status() |
255 if not last_message: | 360 if not last_message: |
256 last_message = current_status.message | 361 last_message = current_status.message |
257 | 362 |
258 template_values = self.InitializeTemplate(self.APP_NAME + ' Tree Status') | 363 template_values = self.InitializeTemplate(self.APP_NAME + ' Tree Status') |
(...skipping 30 matching lines...) Expand all Loading... | |
289 return self._handle(error_message, last_message) | 394 return self._handle(error_message, last_message) |
290 else: | 395 else: |
291 put_status(Status(message=new_message, username=self.user.email())) | 396 put_status(Status(message=new_message, username=self.user.email())) |
292 self.redirect("/") | 397 self.redirect("/") |
293 | 398 |
294 | 399 |
295 def bootstrap(): | 400 def bootstrap(): |
296 # Guarantee that at least one instance exists. | 401 # Guarantee that at least one instance exists. |
297 if db.GqlQuery('SELECT __key__ FROM Status').get() is None: | 402 if db.GqlQuery('SELECT __key__ FROM Status').get() is None: |
298 Status(username='none', message='welcome to status').put() | 403 Status(username='none', message='welcome to status').put() |
OLD | NEW |