Merge lp:~gary/launchpad/bug548-db-3 into lp:launchpad/db-devel
- bug548-db-3
- Merge into db-devel
Status: | Merged |
---|---|
Approved by: | Brad Crittenden |
Approved revision: | no longer in the source branch. |
Merged at revision: | 10177 |
Proposed branch: | lp:~gary/launchpad/bug548-db-3 |
Merge into: | lp:launchpad/db-devel |
Prerequisite: | lp:~wgrant/launchpad/bug548-db-2-tests |
Diff against target: |
1057 lines (+693/-128) 5 files modified
lib/canonical/widgets/__init__.py (+12/-0) lib/contrib/oauth.py (+529/-0) lib/lp/bugs/doc/bugnotification-sending.txt (+91/-118) lib/lp/registry/model/person.py (+55/-8) lib/lp/scripts/garbo.py (+6/-2) |
To merge this branch: | bzr merge lp:~gary/launchpad/bug548-db-3 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Crittenden (community) | code | Approve | |
Review via email:
|
Commit message
[r=bac]
Description of the change
This branch fixes the remaining test failures from wgrant's branch (fixing most of the failures) of my previous branches. It also reinstates the use of a test helper for one file (bugnotificatio
This branch had to fix three sorts of test failures. First, I had hoped that we would be able to make the person-specific settings simply not implemented for teams. This proved to be untenable: at least two automated interface-based functions (verifyObject from zope.interface and lazr.lifecycle's snapshot functionality) crashed and burned by this behavior. Therefore, I opted to make the attributes readonly for teams, using the defaults from the interfaces. I had a mid-implementation call with Danilo about how to tackle that. I want to do what you see here, rather than a __getattr_
- This way keeps you from having to repeat yourself (again) by explicitly listing the attribute names.
- We expect to add many more attributes to this settings bag, and setting up each one manually in a readonly version would be a drag.
- We expect to add a team-based settings bag, and repeating things there would be even more of a drag.
Danilo thought that my preferred approach was acceptable, so I proceeded.
The second test failure was in garbo, cleaning up people. This was simply addressed by teaching it about the new person settings table (that it is safe to ignore it when trying to determine whether a person is still linked).
The last test failure was that ImmutableVisibi
My goal is to get this landed asap to try and make it into PQM. I'd prefer having to make "I'll fix that next" promises if possible, rather than significantly holding up the branch, but I will understand if you are not comfortable with that.
Thank you
Gary
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
- The widgets bits are spurious diff artifacts and are not pertinent to or part of my change.
- I pushed a revert to a comment change in garbo.py to re-correct some grammar.
- I lied. lines 299-319 of the current diff have not been reviewed. They reflect the change making the selfgenerated_
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Brad Crittenden (bac) wrote : | # |
Thanks for the great explanation to a particularly dense piece of code. It is understandable now.
The changes look good. Please do figure out the odd appearance of extra files before sending this up.
Preview Diff
1 | === added directory 'lib/canonical/widgets' |
2 | === added file 'lib/canonical/widgets/__init__.py' |
3 | --- lib/canonical/widgets/__init__.py 1970-01-01 00:00:00 +0000 |
4 | +++ lib/canonical/widgets/__init__.py 2011-02-02 21:39:04 +0000 |
5 | @@ -0,0 +1,12 @@ |
6 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
7 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
8 | + |
9 | +# This is a stub to keep canonical.shipit operational. this module |
10 | +# can delete when shipit is independent. |
11 | + |
12 | +from lp.app.widgets.itemswidgets import ( |
13 | + CheckBoxMatrixWidget, |
14 | + LabeledMultiCheckBoxWidget, |
15 | + ) |
16 | + |
17 | + |
18 | |
19 | === added file 'lib/contrib/oauth.py' |
20 | --- lib/contrib/oauth.py 1970-01-01 00:00:00 +0000 |
21 | +++ lib/contrib/oauth.py 2011-02-04 14:41:18 +0000 |
22 | @@ -0,0 +1,529 @@ |
23 | +# pylint: disable-msg=C0301,E0602,E0211,E0213,W0105,W0231,W0702 |
24 | + |
25 | +import cgi |
26 | +import urllib |
27 | +import time |
28 | +import random |
29 | +import urlparse |
30 | +import hmac |
31 | +import base64 |
32 | + |
33 | +VERSION = '1.0' # Hi Blaine! |
34 | +HTTP_METHOD = 'GET' |
35 | +SIGNATURE_METHOD = 'PLAINTEXT' |
36 | + |
37 | +# Generic exception class |
38 | +class OAuthError(RuntimeError): |
39 | + def __init__(self, message='OAuth error occured'): |
40 | + self.message = message |
41 | + |
42 | +# optional WWW-Authenticate header (401 error) |
43 | +def build_authenticate_header(realm=''): |
44 | + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} |
45 | + |
46 | +# url escape |
47 | +def escape(s): |
48 | + # escape '/' too |
49 | + return urllib.quote(s, safe='~') |
50 | + |
51 | +# util function: current timestamp |
52 | +# seconds since epoch (UTC) |
53 | +def generate_timestamp(): |
54 | + return int(time.time()) |
55 | + |
56 | +# util function: nonce |
57 | +# pseudorandom number |
58 | +def generate_nonce(length=8): |
59 | + return ''.join(str(random.randint(0, 9)) for i in range(length)) |
60 | + |
61 | +# OAuthConsumer is a data type that represents the identity of the Consumer |
62 | +# via its shared secret with the Service Provider. |
63 | +class OAuthConsumer(object): |
64 | + key = None |
65 | + secret = None |
66 | + |
67 | + def __init__(self, key, secret): |
68 | + self.key = key |
69 | + self.secret = secret |
70 | + |
71 | +# OAuthToken is a data type that represents an End User via either an access |
72 | +# or request token. |
73 | +class OAuthToken(object): |
74 | + # access tokens and request tokens |
75 | + key = None |
76 | + secret = None |
77 | + |
78 | + ''' |
79 | + key = the token |
80 | + secret = the token secret |
81 | + ''' |
82 | + def __init__(self, key, secret): |
83 | + self.key = key |
84 | + self.secret = secret |
85 | + |
86 | + def to_string(self): |
87 | + return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) |
88 | + |
89 | + # return a token from something like: |
90 | + # oauth_token_secret=digg&oauth_token=digg |
91 | + @staticmethod |
92 | + def from_string(s): |
93 | + params = cgi.parse_qs(s, keep_blank_values=False) |
94 | + key = params['oauth_token'][0] |
95 | + secret = params['oauth_token_secret'][0] |
96 | + return OAuthToken(key, secret) |
97 | + |
98 | + def __str__(self): |
99 | + return self.to_string() |
100 | + |
101 | +# OAuthRequest represents the request and can be serialized |
102 | +class OAuthRequest(object): |
103 | + ''' |
104 | + OAuth parameters: |
105 | + - oauth_consumer_key |
106 | + - oauth_token |
107 | + - oauth_signature_method |
108 | + - oauth_signature |
109 | + - oauth_timestamp |
110 | + - oauth_nonce |
111 | + - oauth_version |
112 | + ... any additional parameters, as defined by the Service Provider. |
113 | + ''' |
114 | + parameters = None # oauth parameters |
115 | + http_method = HTTP_METHOD |
116 | + http_url = None |
117 | + version = VERSION |
118 | + |
119 | + def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): |
120 | + self.http_method = http_method |
121 | + self.http_url = http_url |
122 | + self.parameters = parameters or {} |
123 | + |
124 | + def set_parameter(self, parameter, value): |
125 | + self.parameters[parameter] = value |
126 | + |
127 | + def get_parameter(self, parameter): |
128 | + try: |
129 | + return self.parameters[parameter] |
130 | + except: |
131 | + raise OAuthError('Parameter not found: %s' % parameter) |
132 | + |
133 | + def _get_timestamp_nonce(self): |
134 | + return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') |
135 | + |
136 | + # get any non-oauth parameters |
137 | + def get_nonoauth_parameters(self): |
138 | + parameters = {} |
139 | + for k, v in self.parameters.iteritems(): |
140 | + # ignore oauth parameters |
141 | + if k.find('oauth_') < 0: |
142 | + parameters[k] = v |
143 | + return parameters |
144 | + |
145 | + # serialize as a header for an HTTPAuth request |
146 | + def to_header(self, realm=''): |
147 | + auth_header = 'OAuth realm="%s"' % realm |
148 | + # add the oauth parameters |
149 | + if self.parameters: |
150 | + for k, v in self.parameters.iteritems(): |
151 | + auth_header += ', %s="%s"' % (k, v) |
152 | + return {'Authorization': auth_header} |
153 | + |
154 | + # serialize as post data for a POST request |
155 | + def to_postdata(self): |
156 | + return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()) |
157 | + |
158 | + # serialize as a url for a GET request |
159 | + def to_url(self): |
160 | + return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) |
161 | + |
162 | + # return a string that consists of all the parameters that need to be signed |
163 | + def get_normalized_parameters(self): |
164 | + params = self.parameters |
165 | + try: |
166 | + # exclude the signature if it exists |
167 | + del params['oauth_signature'] |
168 | + except: |
169 | + pass |
170 | + key_values = params.items() |
171 | + # sort lexicographically, first after key, then after value |
172 | + key_values.sort() |
173 | + # combine key value pairs in string and escape |
174 | + return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values) |
175 | + |
176 | + # just uppercases the http method |
177 | + def get_normalized_http_method(self): |
178 | + return self.http_method.upper() |
179 | + |
180 | + # parses the url and rebuilds it to be scheme://host/path |
181 | + def get_normalized_http_url(self): |
182 | + parts = urlparse.urlparse(self.http_url) |
183 | + url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path |
184 | + return url_string |
185 | + |
186 | + # set the signature parameter to the result of build_signature |
187 | + def sign_request(self, signature_method, consumer, token): |
188 | + # set the signature method |
189 | + self.set_parameter('oauth_signature_method', signature_method.get_name()) |
190 | + # set the signature |
191 | + self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) |
192 | + |
193 | + def build_signature(self, signature_method, consumer, token): |
194 | + # call the build signature method within the signature method |
195 | + return signature_method.build_signature(self, consumer, token) |
196 | + |
197 | + @staticmethod |
198 | + def from_request(http_method, http_url, headers=None, postdata=None, parameters=None): |
199 | + |
200 | + # let the library user override things however they'd like, if they know |
201 | + # which parameters to use then go for it, for example XMLRPC might want to |
202 | + # do this |
203 | + if parameters is not None: |
204 | + return OAuthRequest(http_method, http_url, parameters) |
205 | + |
206 | + # from the headers |
207 | + if headers is not None: |
208 | + try: |
209 | + auth_header = headers['Authorization'] |
210 | + # check that the authorization header is OAuth |
211 | + auth_header.index('OAuth') |
212 | + # get the parameters from the header |
213 | + parameters = OAuthRequest._split_header(auth_header) |
214 | + return OAuthRequest(http_method, http_url, parameters) |
215 | + except: |
216 | + raise OAuthError('Unable to parse OAuth parameters from Authorization header.') |
217 | + |
218 | + # from the parameter string (post body) |
219 | + if http_method == 'POST' and postdata is not None: |
220 | + parameters = OAuthRequest._split_url_string(postdata) |
221 | + |
222 | + # from the url string |
223 | + elif http_method == 'GET': |
224 | + param_str = urlparse.urlparse(http_url).query |
225 | + parameters = OAuthRequest._split_url_string(param_str) |
226 | + |
227 | + if parameters: |
228 | + return OAuthRequest(http_method, http_url, parameters) |
229 | + |
230 | + raise OAuthError('Missing all OAuth parameters. OAuth parameters must be in the headers, post body, or url.') |
231 | + |
232 | + @staticmethod |
233 | + def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): |
234 | + if not parameters: |
235 | + parameters = {} |
236 | + |
237 | + defaults = { |
238 | + 'oauth_consumer_key': oauth_consumer.key, |
239 | + 'oauth_timestamp': generate_timestamp(), |
240 | + 'oauth_nonce': generate_nonce(), |
241 | + 'oauth_version': OAuthRequest.version, |
242 | + } |
243 | + |
244 | + defaults.update(parameters) |
245 | + parameters = defaults |
246 | + |
247 | + if token: |
248 | + parameters['oauth_token'] = token.key |
249 | + |
250 | + return OAuthRequest(http_method, http_url, parameters) |
251 | + |
252 | + @staticmethod |
253 | + def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): |
254 | + if not parameters: |
255 | + parameters = {} |
256 | + |
257 | + parameters['oauth_token'] = token.key |
258 | + |
259 | + if callback: |
260 | + parameters['oauth_callback'] = escape(callback) |
261 | + |
262 | + return OAuthRequest(http_method, http_url, parameters) |
263 | + |
264 | + # util function: turn Authorization: header into parameters, has to do some unescaping |
265 | + @staticmethod |
266 | + def _split_header(header): |
267 | + params = {} |
268 | + header = header.lstrip() |
269 | + if not header.startswith('OAuth '): |
270 | + raise ValueError("not an OAuth header: %r" % header) |
271 | + header = header[6:] |
272 | + parts = header.split(',') |
273 | + for param in parts: |
274 | + # remove whitespace |
275 | + param = param.strip() |
276 | + # split key-value |
277 | + param_parts = param.split('=', 1) |
278 | + if param_parts[0] == 'realm': |
279 | + # Realm header is not an OAuth parameter according to rfc5849 |
280 | + # section 3.4.1.3.1. |
281 | + continue |
282 | + # remove quotes and unescape the value |
283 | + params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) |
284 | + return params |
285 | + |
286 | + # util function: turn url string into parameters, has to do some unescaping |
287 | + @staticmethod |
288 | + def _split_url_string(param_str): |
289 | + parameters = cgi.parse_qs(param_str, keep_blank_values=False) |
290 | + for k, v in parameters.iteritems(): |
291 | + parameters[k] = urllib.unquote(v[0]) |
292 | + return parameters |
293 | + |
294 | +# OAuthServer is a worker to check a requests validity against a data store |
295 | +class OAuthServer(object): |
296 | + timestamp_threshold = 300 # in seconds, five minutes |
297 | + version = VERSION |
298 | + signature_methods = None |
299 | + data_store = None |
300 | + |
301 | + def __init__(self, data_store=None, signature_methods=None): |
302 | + self.data_store = data_store |
303 | + self.signature_methods = signature_methods or {} |
304 | + |
305 | + def set_data_store(self, oauth_data_store): |
306 | + self.data_store = oauth_data_store |
307 | + |
308 | + def get_data_store(self): |
309 | + return self.data_store |
310 | + |
311 | + def add_signature_method(self, signature_method): |
312 | + self.signature_methods[signature_method.get_name()] = signature_method |
313 | + return self.signature_methods |
314 | + |
315 | + # process a request_token request |
316 | + # returns the request token on success |
317 | + def fetch_request_token(self, oauth_request): |
318 | + try: |
319 | + # get the request token for authorization |
320 | + token = self._get_token(oauth_request, 'request') |
321 | + except: |
322 | + # no token required for the initial token request |
323 | + version = self._get_version(oauth_request) |
324 | + consumer = self._get_consumer(oauth_request) |
325 | + self._check_signature(oauth_request, consumer, None) |
326 | + # fetch a new token |
327 | + token = self.data_store.fetch_request_token(consumer) |
328 | + return token |
329 | + |
330 | + # process an access_token request |
331 | + # returns the access token on success |
332 | + def fetch_access_token(self, oauth_request): |
333 | + version = self._get_version(oauth_request) |
334 | + consumer = self._get_consumer(oauth_request) |
335 | + # get the request token |
336 | + token = self._get_token(oauth_request, 'request') |
337 | + self._check_signature(oauth_request, consumer, token) |
338 | + new_token = self.data_store.fetch_access_token(consumer, token) |
339 | + return new_token |
340 | + |
341 | + # verify an api call, checks all the parameters |
342 | + def verify_request(self, oauth_request): |
343 | + # -> consumer and token |
344 | + version = self._get_version(oauth_request) |
345 | + consumer = self._get_consumer(oauth_request) |
346 | + # get the access token |
347 | + token = self._get_token(oauth_request, 'access') |
348 | + self._check_signature(oauth_request, consumer, token) |
349 | + parameters = oauth_request.get_nonoauth_parameters() |
350 | + return consumer, token, parameters |
351 | + |
352 | + # authorize a request token |
353 | + def authorize_token(self, token, user): |
354 | + return self.data_store.authorize_request_token(token, user) |
355 | + |
356 | + # get the callback url |
357 | + def get_callback(self, oauth_request): |
358 | + return oauth_request.get_parameter('oauth_callback') |
359 | + |
360 | + # optional support for the authenticate header |
361 | + def build_authenticate_header(self, realm=''): |
362 | + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} |
363 | + |
364 | + # verify the correct version request for this server |
365 | + def _get_version(self, oauth_request): |
366 | + try: |
367 | + version = oauth_request.get_parameter('oauth_version') |
368 | + except: |
369 | + version = VERSION |
370 | + if version and version != self.version: |
371 | + raise OAuthError('OAuth version %s not supported' % str(version)) |
372 | + return version |
373 | + |
374 | + # figure out the signature with some defaults |
375 | + def _get_signature_method(self, oauth_request): |
376 | + try: |
377 | + signature_method = oauth_request.get_parameter('oauth_signature_method') |
378 | + except: |
379 | + signature_method = SIGNATURE_METHOD |
380 | + try: |
381 | + # get the signature method object |
382 | + signature_method = self.signature_methods[signature_method] |
383 | + except: |
384 | + signature_method_names = ', '.join(self.signature_methods.keys()) |
385 | + raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) |
386 | + |
387 | + return signature_method |
388 | + |
389 | + def _get_consumer(self, oauth_request): |
390 | + consumer_key = oauth_request.get_parameter('oauth_consumer_key') |
391 | + if not consumer_key: |
392 | + raise OAuthError('Invalid consumer key') |
393 | + consumer = self.data_store.lookup_consumer(consumer_key) |
394 | + if not consumer: |
395 | + raise OAuthError('Invalid consumer') |
396 | + return consumer |
397 | + |
398 | + # try to find the token for the provided request token key |
399 | + def _get_token(self, oauth_request, token_type='access'): |
400 | + token_field = oauth_request.get_parameter('oauth_token') |
401 | + token = self.data_store.lookup_token(token_type, token_field) |
402 | + if not token: |
403 | + raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) |
404 | + return token |
405 | + |
406 | + def _check_signature(self, oauth_request, consumer, token): |
407 | + timestamp, nonce = oauth_request._get_timestamp_nonce() |
408 | + self._check_timestamp(timestamp) |
409 | + self._check_nonce(consumer, token, nonce) |
410 | + signature_method = self._get_signature_method(oauth_request) |
411 | + try: |
412 | + signature = oauth_request.get_parameter('oauth_signature') |
413 | + except: |
414 | + raise OAuthError('Missing signature') |
415 | + # attempt to construct the same signature |
416 | + built = signature_method.build_signature(oauth_request, consumer, token) |
417 | + if signature != built: |
418 | + key, base = signature_method.build_signature_base_string(oauth_request, consumer, token) |
419 | + raise OAuthError('Signature does not match. Expected: %s Got: %s Expected signature base string: %s' % (built, signature, base)) |
420 | + |
421 | + def _check_timestamp(self, timestamp): |
422 | + # verify that timestamp is recentish |
423 | + timestamp = int(timestamp) |
424 | + now = int(time.time()) |
425 | + lapsed = now - timestamp |
426 | + if lapsed > self.timestamp_threshold: |
427 | + raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) |
428 | + |
429 | + def _check_nonce(self, consumer, token, nonce): |
430 | + # verify that the nonce is uniqueish |
431 | + try: |
432 | + self.data_store.lookup_nonce(consumer, token, nonce) |
433 | + raise OAuthError('Nonce already used: %s' % str(nonce)) |
434 | + except: |
435 | + pass |
436 | + |
437 | +# OAuthClient is a worker to attempt to execute a request |
438 | +class OAuthClient(object): |
439 | + consumer = None |
440 | + token = None |
441 | + |
442 | + def __init__(self, oauth_consumer, oauth_token): |
443 | + self.consumer = oauth_consumer |
444 | + self.token = oauth_token |
445 | + |
446 | + def get_consumer(self): |
447 | + return self.consumer |
448 | + |
449 | + def get_token(self): |
450 | + return self.token |
451 | + |
452 | + def fetch_request_token(self, oauth_request): |
453 | + # -> OAuthToken |
454 | + raise NotImplementedError |
455 | + |
456 | + def fetch_access_token(self, oauth_request): |
457 | + # -> OAuthToken |
458 | + raise NotImplementedError |
459 | + |
460 | + def access_resource(self, oauth_request): |
461 | + # -> some protected resource |
462 | + raise NotImplementedError |
463 | + |
464 | +# OAuthDataStore is a database abstraction used to lookup consumers and tokens |
465 | +class OAuthDataStore(object): |
466 | + |
467 | + def lookup_consumer(self, key): |
468 | + # -> OAuthConsumer |
469 | + raise NotImplementedError |
470 | + |
471 | + def lookup_token(self, oauth_consumer, token_type, token_token): |
472 | + # -> OAuthToken |
473 | + raise NotImplementedError |
474 | + |
475 | + def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): |
476 | + # -> OAuthToken |
477 | + raise NotImplementedError |
478 | + |
479 | + def fetch_request_token(self, oauth_consumer): |
480 | + # -> OAuthToken |
481 | + raise NotImplementedError |
482 | + |
483 | + def fetch_access_token(self, oauth_consumer, oauth_token): |
484 | + # -> OAuthToken |
485 | + raise NotImplementedError |
486 | + |
487 | + def authorize_request_token(self, oauth_token, user): |
488 | + # -> OAuthToken |
489 | + raise NotImplementedError |
490 | + |
491 | +# OAuthSignatureMethod is a strategy class that implements a signature method |
492 | +class OAuthSignatureMethod(object): |
493 | + def get_name(): |
494 | + # -> str |
495 | + raise NotImplementedError |
496 | + |
497 | + def build_signature_base_string(oauth_request, oauth_consumer, oauth_token): |
498 | + # -> str key, str raw |
499 | + raise NotImplementedError |
500 | + |
501 | + def build_signature(oauth_request, oauth_consumer, oauth_token): |
502 | + # -> str |
503 | + raise NotImplementedError |
504 | + |
505 | +class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): |
506 | + |
507 | + def get_name(self): |
508 | + return 'HMAC-SHA1' |
509 | + |
510 | + def build_signature_base_string(self, oauth_request, consumer, token): |
511 | + sig = ( |
512 | + escape(oauth_request.get_normalized_http_method()), |
513 | + escape(oauth_request.get_normalized_http_url()), |
514 | + escape(oauth_request.get_normalized_parameters()), |
515 | + ) |
516 | + |
517 | + key = '%s&' % escape(consumer.secret) |
518 | + if token: |
519 | + key += escape(token.secret) |
520 | + raw = '&'.join(sig) |
521 | + return key, raw |
522 | + |
523 | + def build_signature(self, oauth_request, consumer, token): |
524 | + # build the base signature string |
525 | + key, raw = self.build_signature_base_string(oauth_request, consumer, token) |
526 | + |
527 | + # hmac object |
528 | + try: |
529 | + import hashlib # 2.5 |
530 | + hashed = hmac.new(key, raw, hashlib.sha1) |
531 | + except: |
532 | + import sha # deprecated |
533 | + hashed = hmac.new(key, raw, sha) |
534 | + |
535 | + # calculate the digest base 64 |
536 | + return base64.b64encode(hashed.digest()) |
537 | + |
538 | +class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): |
539 | + |
540 | + def get_name(self): |
541 | + return 'PLAINTEXT' |
542 | + |
543 | + def build_signature_base_string(self, oauth_request, consumer, token): |
544 | + # concatenate the consumer key and secret |
545 | + sig = escape(consumer.secret) |
546 | + if token: |
547 | + sig = '&'.join((sig, escape(token.secret))) |
548 | + return sig |
549 | + |
550 | + def build_signature(self, oauth_request, consumer, token): |
551 | + return self.build_signature_base_string(oauth_request, consumer, token) |
552 | |
553 | === modified file 'lib/lp/bugs/doc/bugnotification-sending.txt' |
554 | --- lib/lp/bugs/doc/bugnotification-sending.txt 2011-02-04 20:32:01 +0000 |
555 | +++ lib/lp/bugs/doc/bugnotification-sending.txt 2011-02-04 20:32:11 +0000 |
556 | @@ -30,20 +30,9 @@ |
557 | ... print email_notification.get_payload(decode=True) |
558 | ... print "-" * 70 |
559 | |
560 | -We'll also define some helper functions to help us with database users. |
561 | - |
562 | - >>> from canonical.config import config |
563 | - >>> from canonical.database.sqlbase import commit |
564 | - >>> from canonical.testing.layers import LaunchpadZopelessLayer |
565 | - |
566 | - >>> def switch_db_to_launchpad(): |
567 | - ... commit() |
568 | - ... LaunchpadZopelessLayer.switchDbUser('launchpad') |
569 | - |
570 | - >>> def switch_db_to_bugnotification(): |
571 | - ... commit() |
572 | - ... LaunchpadZopelessLayer.switchDbUser( |
573 | - ... config.malone.bugnotification_dbuser) |
574 | +We'll also import a helper function to help us with database users. |
575 | + |
576 | + >>> from lp.testing.dbuser import lp_dbuser |
577 | |
578 | You'll note that we are printing out an X-Launchpad-Message-Rationale |
579 | header. This header is a simple string that allows people to filter |
580 | @@ -356,16 +345,13 @@ |
581 | ... print member.preferredemail.email |
582 | marilize@hbd.com |
583 | |
584 | - >>> switch_db_to_launchpad() |
585 | - >>> bug_one.subscribe(shipit_admins, shipit_admins) |
586 | - <...> |
587 | - |
588 | - >>> comment = getUtility(IMessageSet).fromText( |
589 | - ... 'subject', 'a comment.', sample_person, |
590 | - ... datecreated=ten_minutes_ago) |
591 | - >>> bug_one.addCommentNotification(comment) |
592 | - |
593 | - >>> switch_db_to_bugnotification() |
594 | + >>> with lp_dbuser(): |
595 | + ... ignored = bug_one.subscribe(shipit_admins, shipit_admins) |
596 | + ... comment = getUtility(IMessageSet).fromText( |
597 | + ... 'subject', 'a comment.', sample_person, |
598 | + ... datecreated=ten_minutes_ago) |
599 | + ... bug_one.addCommentNotification(comment) |
600 | + |
601 | >>> pending_notifications = getUtility( |
602 | ... IBugNotificationSet).getNotificationsToSend() |
603 | >>> len(pending_notifications) |
604 | @@ -398,17 +384,15 @@ |
605 | >>> params = CreateBugParams( |
606 | ... msg=description, owner=sample_person, title='new bug') |
607 | |
608 | - >>> switch_db_to_launchpad() |
609 | - >>> new_bug = ubuntu.createBug(params) |
610 | - >>> switch_db_to_bugnotification() |
611 | - >>> flush_notifications() |
612 | + >>> with lp_dbuser(): |
613 | + ... new_bug = ubuntu.createBug(params) |
614 | |
615 | If a bug is a duplicate of another bug, a marker gets inserted at the |
616 | top of the email: |
617 | |
618 | - >>> switch_db_to_launchpad() |
619 | - >>> new_bug.markAsDuplicate(bug_one) |
620 | - >>> switch_db_to_bugnotification() |
621 | + >>> flush_notifications() |
622 | + >>> with lp_dbuser(): |
623 | + ... new_bug.markAsDuplicate(bug_one) |
624 | >>> comment = getUtility(IMessageSet).fromText( |
625 | ... 'subject', 'a comment.', sample_person, |
626 | ... datecreated=ten_minutes_ago) |
627 | @@ -474,12 +458,11 @@ |
628 | ... 'Zero-day on Frobulator', 'Woah.', sample_person, |
629 | ... datecreated=ten_minutes_ago) |
630 | |
631 | - >>> switch_db_to_launchpad() |
632 | - >>> sec_vuln_bug = ubuntu.createBug(CreateBugParams( |
633 | + >>> with lp_dbuser(): |
634 | + ... sec_vuln_bug = ubuntu.createBug(CreateBugParams( |
635 | ... msg=sec_vuln_description, owner=sample_person, |
636 | ... title='Zero-day on Frobulator', |
637 | ... security_related=True, private=True)) |
638 | - >>> switch_db_to_bugnotification() |
639 | |
640 | >>> sec_vuln_bug.security_related |
641 | True |
642 | @@ -730,10 +713,8 @@ |
643 | The tags will be space-separated to allow the list to be wrapped if it |
644 | gets over-long. |
645 | |
646 | - >>> switch_db_to_launchpad() |
647 | - >>> bug_three.tags = [u'layout-test', u'another-tag', u'yet-another'] |
648 | - |
649 | - >>> switch_db_to_bugnotification() |
650 | + >>> with lp_dbuser(): |
651 | + ... bug_three.tags = [u'layout-test', u'another-tag', u'yet-another'] |
652 | |
653 | >>> bug_three = getUtility(IBugSet).get(3) |
654 | >>> for message in trigger_and_get_email_messages(bug_three): |
655 | @@ -743,15 +724,13 @@ |
656 | If we remove the tags from the bug, the X-Launchpad-Bug-Tags header |
657 | won't be included. |
658 | |
659 | - >>> switch_db_to_launchpad() |
660 | - >>> bug_three.tags = [] |
661 | - >>> switch_db_to_bugnotification() |
662 | + >>> with lp_dbuser(): |
663 | + ... bug_three.tags = [] |
664 | |
665 | >>> bug_three = getUtility(IBugSet).get(3) |
666 | >>> for message in trigger_and_get_email_messages(bug_three): |
667 | ... message.get_all('X-Launchpad-Bug-Tags') |
668 | |
669 | - >>> switch_db_to_launchpad() |
670 | >>> #bug_three.unsubscribe(sample_person, sample_person) |
671 | |
672 | |
673 | @@ -771,7 +750,8 @@ |
674 | |
675 | Predictably, private bugs are sent with a slightly different header: |
676 | |
677 | - >>> bug_three.setPrivate(True, sample_person) |
678 | + >>> with lp_dbuser(): |
679 | + ... bug_three.setPrivate(True, sample_person) |
680 | True |
681 | >>> bug_three.private |
682 | True |
683 | @@ -799,7 +779,8 @@ |
684 | The presence of the security flag on a bug is, surprise, denoted by a |
685 | simple "yes": |
686 | |
687 | - >>> bug_three.setSecurityRelated(True) |
688 | + >>> with lp_dbuser(): |
689 | + ... bug_three.setSecurityRelated(True) |
690 | True |
691 | >>> bug_three.security_related |
692 | True |
693 | @@ -824,9 +805,9 @@ |
694 | >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com') |
695 | |
696 | >>> from lp.bugs.interfaces.bugmessage import IBugMessageSet |
697 | - >>> getUtility(IBugMessageSet).createMessage( |
698 | - ... 'Hungry', bug_three, foo_bar, "Make me a sandwich.") |
699 | - <BugMessage ...> |
700 | + >>> with lp_dbuser(): |
701 | + ... ignored = getUtility(IBugMessageSet).createMessage( |
702 | + ... 'Hungry', bug_three, foo_bar, "Make me a sandwich.") |
703 | |
704 | >>> for message in trigger_and_get_email_messages(bug_three): |
705 | ... print message.get('X-Launchpad-Bug-Commenters') |
706 | @@ -835,9 +816,9 @@ |
707 | It only lists each user once, no matter how many comments they've |
708 | made. |
709 | |
710 | - >>> getUtility(IBugMessageSet).createMessage( |
711 | - ... 'Hungry', bug_three, foo_bar, "Make me a sandwich.") |
712 | - <BugMessage ...> |
713 | + >>> with lp_dbuser(): |
714 | + ... ignored = getUtility(IBugMessageSet).createMessage( |
715 | + ... 'Hungry', bug_three, foo_bar, "Make me a sandwich.") |
716 | |
717 | >>> for message in trigger_and_get_email_messages(bug_three): |
718 | ... print message.get('X-Launchpad-Bug-Commenters') |
719 | @@ -868,63 +849,58 @@ |
720 | Concise Person does not. We'll also create teams and give them members |
721 | with different verbose_bugnotifications settings. |
722 | |
723 | - >>> switch_db_to_launchpad() |
724 | - >>> bug = factory.makeBug( |
725 | - ... product=factory.makeProduct(title='Foo'), |
726 | - ... title='In the beginning, the universe was created. This ' |
727 | - ... 'has made a lot of people very angry and has been ' |
728 | - ... 'widely regarded as a bad move', |
729 | - ... description="This is a long description of the bug, which " |
730 | - ... "will be automatically wrapped by the BugNotification " |
731 | - ... "machinery. Ain't technology great?") |
732 | - |
733 | - >>> verbose_person = factory.makePerson( |
734 | - ... displayname='Verbose Person', email='verbose@example.com') |
735 | - >>> verbose_person.verbose_bugnotifications = True |
736 | - >>> bug.subscribe(verbose_person, verbose_person) |
737 | - <lp.bugs.model.bugsubscription.BugSubscription ...> |
738 | - |
739 | - >>> concise_person = factory.makePerson( |
740 | - ... displayname='Concise Person', email='concise@example.com') |
741 | - >>> concise_person.verbose_bugnotifications = False |
742 | - >>> bug.subscribe(concise_person, concise_person) |
743 | - <lp.bugs.model.bugsubscription.BugSubscription ...> |
744 | + >>> with lp_dbuser(): |
745 | + ... bug = factory.makeBug( |
746 | + ... product=factory.makeProduct(title='Foo'), |
747 | + ... title='In the beginning, the universe was created. This ' |
748 | + ... 'has made a lot of people very angry and has been ' |
749 | + ... 'widely regarded as a bad move', |
750 | + ... description="This is a long description of the bug, which " |
751 | + ... "will be automatically wrapped by the BugNotification " |
752 | + ... "machinery. Ain't technology great?") |
753 | + ... verbose_person = factory.makePerson( |
754 | + ... displayname='Verbose Person', email='verbose@example.com') |
755 | + ... verbose_person.verbose_bugnotifications = True |
756 | + ... ignored = bug.subscribe(verbose_person, verbose_person) |
757 | + ... concise_person = factory.makePerson( |
758 | + ... displayname='Concise Person', email='concise@example.com') |
759 | + ... concise_person.verbose_bugnotifications = False |
760 | + ... ignored = bug.subscribe(concise_person, concise_person) |
761 | |
762 | |
763 | Concise Team doesn't want verbose notifications, while Concise Team |
764 | Person, a member, does. |
765 | |
766 | - >>> concise_team = factory.makeTeam( |
767 | - ... name='conciseteam', displayname='Concise Team') |
768 | - >>> concise_team.verbose_bugnotifications = False |
769 | - >>> concise_team_person = factory.makePerson( |
770 | - ... displayname='Concise Team Person', |
771 | - ... email='conciseteam@example.com') |
772 | - >>> concise_team_person.verbose_bugnotifications = True |
773 | - >>> ignored = concise_team.addMember( |
774 | - ... concise_team_person, concise_team_person) |
775 | - >>> bug.subscribe(concise_team, concise_team_person) |
776 | - <lp.bugs.model.bugsubscription.BugSubscription ...> |
777 | + >>> with lp_dbuser(): |
778 | + ... concise_team = factory.makeTeam( |
779 | + ... name='conciseteam', displayname='Concise Team') |
780 | + ... concise_team.verbose_bugnotifications = False |
781 | + ... concise_team_person = factory.makePerson( |
782 | + ... displayname='Concise Team Person', |
783 | + ... email='conciseteam@example.com') |
784 | + ... concise_team_person.verbose_bugnotifications = True |
785 | + ... ignored = concise_team.addMember( |
786 | + ... concise_team_person, concise_team_person) |
787 | + ... ignored = bug.subscribe(concise_team, concise_team_person) |
788 | |
789 | Verbose Team wants verbose notifications, while Verbose Team Person, a |
790 | member, does not. |
791 | |
792 | - >>> verbose_team = factory.makeTeam( |
793 | - ... name='verboseteam', displayname='Verbose Team') |
794 | - >>> verbose_team.verbose_bugnotifications = True |
795 | - >>> verbose_team_person = factory.makePerson( |
796 | - ... displayname='Verbose Team Person', |
797 | - ... email='verboseteam@example.com') |
798 | - >>> verbose_team_person.verbose_bugnotifications = False |
799 | - >>> ignored = verbose_team.addMember( |
800 | - ... verbose_team_person, verbose_team_person) |
801 | - >>> bug.subscribe(verbose_team, verbose_team_person) |
802 | - <lp.bugs.model.bugsubscription.BugSubscription ...> |
803 | + >>> with lp_dbuser(): |
804 | + ... verbose_team = factory.makeTeam( |
805 | + ... name='verboseteam', displayname='Verbose Team') |
806 | + ... verbose_team.verbose_bugnotifications = True |
807 | + ... verbose_team_person = factory.makePerson( |
808 | + ... displayname='Verbose Team Person', |
809 | + ... email='verboseteam@example.com') |
810 | + ... verbose_team_person.verbose_bugnotifications = False |
811 | + ... ignored = verbose_team.addMember( |
812 | + ... verbose_team_person, verbose_team_person) |
813 | + ... ignored = bug.subscribe(verbose_team, verbose_team_person) |
814 | |
815 | We'll expire all existing notifications since we're not interested in |
816 | them: |
817 | |
818 | - >>> switch_db_to_bugnotification() |
819 | >>> notifications = getUtility( |
820 | ... IBugNotificationSet).getNotificationsToSend() |
821 | >>> len(notifications) |
822 | @@ -1103,19 +1079,22 @@ |
823 | |
824 | People (not teams) will have the choice to receive notifications from actions |
825 | they generated. For now, everyone receives these notifications whether they |
826 | -want them or not. However, we can show that the attribute exists for now. |
827 | +want them or not. |
828 | |
829 | - >>> flush_notifications() |
830 | - >>> switch_db_to_launchpad() |
831 | >>> verbose_person.selfgenerated_bugnotifications |
832 | True |
833 | + >>> with lp_dbuser(): |
834 | + ... verbose_person.selfgenerated_bugnotifications = False |
835 | |
836 | -Teams do not implement this attribute. |
837 | +Teams provide this attribute read-only. |
838 | |
839 | >>> verbose_team.selfgenerated_bugnotifications |
840 | + True |
841 | + >>> with lp_dbuser(): |
842 | + ... verbose_team.selfgenerated_bugnotifications = False |
843 | Traceback (most recent call last): |
844 | ... |
845 | - NotImplementedError: Teams do not support this attribute. |
846 | + NotImplementedError: Teams do not support changing this attribute. |
847 | |
848 | Notification Recipients |
849 | ----------------------- |
850 | @@ -1125,16 +1104,14 @@ |
851 | notification level of the subscription. |
852 | |
853 | >>> flush_notifications() |
854 | - >>> switch_db_to_launchpad() |
855 | |
856 | >>> from lp.bugs.enum import BugNotificationLevel |
857 | >>> from lp.registry.interfaces.product import IProductSet |
858 | >>> firefox = getUtility(IProductSet).getByName('firefox') |
859 | >>> mr_no_privs = getUtility(IPersonSet).getByName('no-priv') |
860 | - >>> subscription_no_priv = firefox.addBugSubscription( |
861 | - ... mr_no_privs, mr_no_privs) |
862 | - |
863 | - >>> switch_db_to_bugnotification() |
864 | + >>> with lp_dbuser(): |
865 | + ... subscription_no_priv = firefox.addBugSubscription( |
866 | + ... mr_no_privs, mr_no_privs) |
867 | |
868 | The notifications generated by addCommentNotification() are sent only to |
869 | structural subscribers with no filters, or with the notification level |
870 | @@ -1200,10 +1177,9 @@ |
871 | |
872 | |
873 | >>> flush_notifications() |
874 | - >>> switch_db_to_launchpad() |
875 | - >>> filter = subscription_no_priv.newBugFilter() |
876 | - >>> filter.bug_notification_level = BugNotificationLevel.COMMENTS |
877 | - >>> switch_db_to_bugnotification() |
878 | + >>> with lp_dbuser(): |
879 | + ... filter = subscription_no_priv.newBugFilter() |
880 | + ... filter.bug_notification_level = BugNotificationLevel.COMMENTS |
881 | |
882 | >>> comment = getUtility(IMessageSet).fromText( |
883 | ... 'subject', 'another comment.', sample_person, |
884 | @@ -1261,9 +1237,8 @@ |
885 | no comment notifications. |
886 | |
887 | >>> flush_notifications() |
888 | - >>> switch_db_to_launchpad() |
889 | - >>> filter.bug_notification_level = BugNotificationLevel.METADATA |
890 | - >>> switch_db_to_bugnotification() |
891 | + >>> with lp_dbuser(): |
892 | + ... filter.bug_notification_level = BugNotificationLevel.METADATA |
893 | |
894 | >>> comment = getUtility(IMessageSet).fromText( |
895 | ... 'subject', 'no comment for no-priv.', sample_person, |
896 | @@ -1373,9 +1348,8 @@ |
897 | no notifications created by addChangeNotification(). |
898 | |
899 | >>> flush_notifications() |
900 | - >>> switch_db_to_launchpad() |
901 | - >>> filter.bug_notification_level = BugNotificationLevel.LIFECYCLE |
902 | - >>> switch_db_to_bugnotification() |
903 | + >>> with lp_dbuser(): |
904 | + ... filter.bug_notification_level = BugNotificationLevel.LIFECYCLE |
905 | |
906 | >>> bug_one.addChangeNotification('** Summary changed to: something.', |
907 | ... sample_person, when=ten_minutes_ago) |
908 | @@ -1433,10 +1407,9 @@ |
909 | after all. |
910 | |
911 | >>> flush_notifications() |
912 | - >>> switch_db_to_launchpad() |
913 | - >>> filter2 = subscription_no_priv.newBugFilter() |
914 | - >>> filter2.bug_notification_level = BugNotificationLevel.METADATA |
915 | - >>> switch_db_to_bugnotification() |
916 | + >>> with lp_dbuser(): |
917 | + ... filter2 = subscription_no_priv.newBugFilter() |
918 | + ... filter2.bug_notification_level = BugNotificationLevel.METADATA |
919 | |
920 | >>> bug_one.addChangeNotification('** Summary changed to: whatever.', |
921 | ... sample_person, when=ten_minutes_ago) |
922 | |
923 | === modified file 'lib/lp/registry/model/person.py' |
924 | --- lib/lp/registry/model/person.py 2011-02-04 20:32:01 +0000 |
925 | +++ lib/lp/registry/model/person.py 2011-02-04 20:32:11 +0000 |
926 | @@ -37,7 +37,6 @@ |
927 | import weakref |
928 | |
929 | from lazr.delegates import delegates |
930 | -from lazr.restful.fields import Reference |
931 | import pytz |
932 | from sqlobject import ( |
933 | BoolCol, |
934 | @@ -84,6 +83,7 @@ |
935 | from zope.event import notify |
936 | from zope.interface import ( |
937 | alsoProvides, |
938 | + classImplements, |
939 | implementer, |
940 | implements, |
941 | ) |
942 | @@ -361,6 +361,7 @@ |
943 | |
944 | |
945 | class PersonSettings(Storm): |
946 | + "The relatively rarely used settings for person (not a team)." |
947 | |
948 | implements(IPersonSettings) |
949 | |
950 | @@ -372,6 +373,53 @@ |
951 | selfgenerated_bugnotifications = BoolCol(notNull=True, default=True) |
952 | |
953 | |
954 | +def readonly_settings(message, interface): |
955 | + """Make an object that disallows writes to values on the interface. |
956 | + |
957 | + When you write, the message is raised in a NotImplementedError. |
958 | + """ |
959 | + # We will make a class that has properties for each field on the |
960 | + # interface (we expect every name on the interface to correspond to a |
961 | + # zope.schema field). Each property will have a getter that will |
962 | + # return the interface default for that name; and it will have a |
963 | + # setter that will raise a hopefully helpful error message |
964 | + # explaining why writing is not allowed. |
965 | + # This is the setter we are going to use for every property. |
966 | + def unwritable(self, value): |
967 | + raise NotImplementedError(message) |
968 | + # This will become the dict of the class. |
969 | + data = {} |
970 | + # The interface.names() method returns the names on the interface. If |
971 | + # "all" is True, then you will also get the names on base |
972 | + # interfaces. That is unlikely to be needed here, but would be the |
973 | + # expected behavior if it were. |
974 | + for name in interface.names(all=True): |
975 | + # This next line is a work-around for a classic problem of |
976 | + # closures in a loop. Closures are bound (indirectly) to frame |
977 | + # locals, which are a mutable collection. Therefore, if we |
978 | + # naively make closures for each different value within a loop, |
979 | + # each closure will be bound to the value as it is at the *end |
980 | + # of the loop*. That's usually not what we want. To prevent |
981 | + # this, we make a helper function (which has its own locals) |
982 | + # that returns the actual closure we want. |
983 | + closure_maker = lambda result: lambda self: result |
984 | + # Now we make a property with the name-specific getter and the generic |
985 | + # setter, and put it in the dictionary of the class we are making. |
986 | + data[name] = property( |
987 | + closure_maker(interface[name].default), unwritable) |
988 | + # Now we have all the attributes we want. We will make the class... |
989 | + cls = type('Unwritable' + interface.__name__, (), data) |
990 | + # ...specify that the class implements the interface that we are working |
991 | + # with... |
992 | + classImplements(cls, interface) |
993 | + # ...and return an instance. We should only need one, since it is |
994 | + # read-only. |
995 | + return cls() |
996 | + |
997 | +_readonly_person_settings = readonly_settings( |
998 | + 'Teams do not support changing this attribute.', IPersonSettings) |
999 | + |
1000 | + |
1001 | class Person( |
1002 | SQLBase, HasBugsBase, HasSpecificationsMixin, HasTranslationImportsMixin, |
1003 | HasBranchesMixin, HasMergeProposalsMixin, HasRequestedReviewsMixin): |
1004 | @@ -391,13 +439,11 @@ |
1005 | @cachedproperty |
1006 | def _person_settings(self): |
1007 | if self.is_team: |
1008 | - # Hopefully no-one ever encounters this. If someone does, |
1009 | - # that means that the code is trying to look at |
1010 | - # person-specific attributes on a team, and we should warn |
1011 | - # about that explicitly to give a hint about what is wrong |
1012 | - # (rather than merely returning None). |
1013 | - raise NotImplementedError( |
1014 | - 'Teams do not support this attribute.') |
1015 | + # Teams need to provide these attributes for reading in order for |
1016 | + # things like snapshots to work, but they are not actually |
1017 | + # pertinent to teams, so they are not actually implemented for |
1018 | + # writes. |
1019 | + return _readonly_person_settings |
1020 | else: |
1021 | # This is a person. |
1022 | return IStore(PersonSettings).find( |
1023 | @@ -2112,6 +2158,7 @@ |
1024 | ('logintoken', 'requester'), |
1025 | ('personlanguage', 'person'), |
1026 | ('personlocation', 'person'), |
1027 | + ('personsettings', 'person'), |
1028 | ('persontransferjob', 'minor_person'), |
1029 | ('persontransferjob', 'major_person'), |
1030 | ('signedcodeofconduct', 'owner'), |
1031 | |
1032 | === modified file 'lib/lp/scripts/garbo.py' |
1033 | --- lib/lp/scripts/garbo.py 2011-01-31 09:24:13 +0000 |
1034 | +++ lib/lp/scripts/garbo.py 2011-02-04 20:32:11 +0000 |
1035 | @@ -432,9 +432,12 @@ |
1036 | postgresql.listReferences(cursor(), 'person', 'id')): |
1037 | # Skip things that don't link to Person.id or that link to it from |
1038 | # TeamParticipation or EmailAddress, as all Person entries will be |
1039 | - # linked to from these tables. |
1040 | + # linked to from these tables. Similarly, PersonSettings can |
1041 | + # simply be deleted if it exists, because it has a 1 (or 0) to 1 |
1042 | + # relationship with Person. |
1043 | if (to_table != 'person' or to_column != 'id' |
1044 | - or from_table in ('teamparticipation', 'emailaddress')): |
1045 | + or from_table in ('teamparticipation', 'emailaddress', |
1046 | + 'personsettings')): |
1047 | continue |
1048 | self.log.debug( |
1049 | "Populating LinkedPeople from %s.%s" |
1050 | @@ -510,6 +513,7 @@ |
1051 | UPDATE EmailAddress SET person=NULL |
1052 | WHERE person IN (%s) |
1053 | """ % people_ids) |
1054 | + # This cascade deletes any PersonSettings records. |
1055 | self.store.execute(""" |
1056 | DELETE FROM Person |
1057 | WHERE id IN (%s) |
Lines 1 - 396 have been reviewed before, though comments/ requests/ suggestions are welcome.