Merge lp:~brian-murray/launchpad/api-export-messages-count into lp:launchpad/db-devel

Proposed by Brian Murray
Status: Merged
Merged at revision: not available
Proposed branch: lp:~brian-murray/launchpad/api-export-messages-count
Merge into: lp:launchpad/db-devel
Diff against target: 1855 lines (+581/-358)
42 files modified
configs/development/launchpad-lazr.conf (+0/-2)
configs/replicated-development/launchpad-lazr.conf (+0/-3)
configs/test-playground/launchpad-lazr.conf (+0/-2)
configs/testrunner/launchpad-lazr.conf (+0/-2)
database/schema/security.cfg (+6/-2)
lib/canonical/config/__init__.py (+2/-2)
lib/canonical/config/schema-lazr.conf (+0/-8)
lib/canonical/configure.zcml (+0/-2)
lib/canonical/database/harness.py (+1/-2)
lib/canonical/database/sqlbase.py (+2/-4)
lib/canonical/database/tests/test_zopeless_transaction_manager.py (+0/-31)
lib/canonical/launchpad/database/tests/test_oauth.py (+1/-1)
lib/canonical/launchpad/doc/account.txt (+1/-26)
lib/canonical/launchpad/doc/storm.txt (+51/-71)
lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt (+0/-2)
lib/canonical/launchpad/scripts/garbo.py (+5/-13)
lib/canonical/launchpad/scripts/tests/test_garbo.py (+7/-9)
lib/canonical/launchpad/webapp/adapter.py (+5/-16)
lib/canonical/launchpad/webapp/ftests/test_adapter_permissions.txt (+1/-24)
lib/canonical/launchpad/webapp/interfaces.py (+1/-3)
lib/canonical/launchpad/webapp/tests/test_dbpolicy.py (+1/-4)
lib/lp/bugs/doc/checkwatches.txt (+4/-0)
lib/lp/bugs/doc/externalbugtracker-comment-imports.txt (+3/-3)
lib/lp/bugs/interfaces/bug.py (+3/-3)
lib/lp/bugs/stories/webservice/xx-bug.txt (+1/-0)
lib/lp/code/doc/branch-karma.txt (+1/-0)
lib/lp/registry/configure.zcml (+47/-5)
lib/lp/registry/doc/distribution-mirror.txt (+119/-1)
lib/lp/registry/interfaces/distribution.py (+9/-1)
lib/lp/registry/interfaces/distributionmirror.py (+96/-47)
lib/lp/registry/model/distribution.py (+11/-0)
lib/lp/registry/model/distributionmirror.py (+63/-1)
lib/lp/registry/model/person.py (+2/-17)
lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt (+11/-11)
lib/lp/registry/stories/person/xx-admin-person-review.txt (+1/-1)
lib/lp/registry/stories/webservice/xx-distribution-mirror.txt (+20/-11)
lib/lp/registry/stories/webservice/xx-distribution.txt (+74/-0)
lib/lp/registry/tests/test_distributionmirror.py (+4/-14)
lib/lp/registry/tests/test_personset.py (+6/-10)
lib/lp/testing/factory.py (+16/-2)
lib/lp/testopenid/browser/server.py (+6/-1)
utilities/sourcedeps.conf (+0/-1)
To merge this branch: bzr merge lp:~brian-murray/launchpad/api-export-messages-count
Reviewer Review Type Date Requested Status
Eleanor Berger (community) Approve
Review via email: mp+17697@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Brian Murray (brian-murray) wrote :

This branch exports a bug's message_count in the API.

Revision history for this message
Eleanor Berger (intellectronica) wrote :

r=me

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'configs/development/launchpad-lazr.conf'
2--- configs/development/launchpad-lazr.conf 2010-03-05 20:49:31 +0000
3+++ configs/development/launchpad-lazr.conf 2010-04-05 20:44:34 +0000
4@@ -97,8 +97,6 @@
5 # the rw_* configs.
6 ro_main_master: dbname=launchpad_dev_template
7 ro_main_slave: dbname=launchpad_dev_template
8-auth_master: dbname=launchpad_dev
9-auth_slave: dbname=launchpad_dev
10
11 [distributionmirrorprober]
12 use_proxy: False
13
14=== modified file 'configs/replicated-development/launchpad-lazr.conf'
15--- configs/replicated-development/launchpad-lazr.conf 2010-01-05 19:09:58 +0000
16+++ configs/replicated-development/launchpad-lazr.conf 2010-04-05 20:44:34 +0000
17@@ -10,6 +10,3 @@
18 rw_main_slave: dbname=launchpad_dev_slave
19 ro_main_master: dbname=launchpad_dev
20 ro_main_slave: dbname=launchpad_dev_slave
21-auth_master: dbname=launchpad_dev
22-auth_slave: dbname=launchpad_dev_slave
23-
24
25=== modified file 'configs/test-playground/launchpad-lazr.conf'
26--- configs/test-playground/launchpad-lazr.conf 2010-01-05 19:09:58 +0000
27+++ configs/test-playground/launchpad-lazr.conf 2010-04-05 20:44:34 +0000
28@@ -10,5 +10,3 @@
29 rw_main_slave: dbname=launchpad_ftest_playground
30 ro_main_master: dbname=launchpad_ftest_playground
31 ro_main_slave: dbname=launchpad_ftest_playground
32-auth_master: dbname=launchpad_ftest_playground
33-auth_slave: dbname=launchpad_ftest_playground
34
35=== modified file 'configs/testrunner/launchpad-lazr.conf'
36--- configs/testrunner/launchpad-lazr.conf 2010-03-11 01:39:25 +0000
37+++ configs/testrunner/launchpad-lazr.conf 2010-04-05 20:44:34 +0000
38@@ -51,8 +51,6 @@
39 # the rw_* configs.
40 ro_main_master: dbname=launchpad_ftest_template
41 ro_main_slave: dbname=launchpad_ftest_template
42-auth_master: dbname=launchpad_ftest
43-auth_slave: dbname=launchpad_ftest
44 randomise_select_results: true
45
46 [error_reports]
47
48=== modified file 'database/schema/security.cfg'
49--- database/schema/security.cfg 2010-04-02 17:29:13 +0000
50+++ database/schema/security.cfg 2010-04-05 20:44:34 +0000
51@@ -118,7 +118,8 @@
52 # lpmain replication set access from the main Z3 application.
53 type=user
54 groups=write,script
55-public.account = SELECT
56+public.account = SELECT, INSERT, UPDATE, DELETE
57+public.accountpassword = SELECT, INSERT, UPDATE, DELETE
58 public.announcement = SELECT, INSERT, UPDATE, DELETE
59 public.answercontact = SELECT, INSERT, UPDATE, DELETE
60 public.apportjob = SELECT, INSERT, UPDATE, DELETE
61@@ -176,7 +177,7 @@
62 public.distributionsourcepackagecache = SELECT
63 public.distroserieslanguage = SELECT, INSERT, UPDATE
64 public.distroseriespackagecache = SELECT
65-public.emailaddress = SELECT
66+public.emailaddress = SELECT, INSERT, UPDATE, DELETE
67 public.entitlement = SELECT, INSERT, UPDATE, DELETE
68 public.faq = SELECT, INSERT, UPDATE, DELETE
69 public.featuredproject = SELECT, INSERT, DELETE
70@@ -916,6 +917,8 @@
71 # Full access except for tables that are exclusively updated by
72 # certain processes, such as the librarian tables. This group is deprecated -
73 # access should be explicitly granted to users.
74+public.account = SELECT, INSERT, UPDATE
75+public.accountpassword = SELECT, INSERT
76 public.archive = SELECT, INSERT, UPDATE
77 public.archivearch = SELECT, INSERT, UPDATE, DELETE
78 public.binarypackagerelease = SELECT, INSERT, UPDATE
79@@ -949,6 +952,7 @@
80 public.distribution = SELECT, INSERT, UPDATE
81 public.distroarchseries = SELECT, INSERT, UPDATE
82 public.distroseries = SELECT, INSERT, UPDATE
83+public.openidrpsummary = SELECT, INSERT, UPDATE
84 public.packageupload = SELECT, INSERT, UPDATE
85 public.packageuploadbuild = SELECT, INSERT, UPDATE
86 public.packageuploadsource = SELECT, INSERT, UPDATE
87
88=== modified file 'lib/canonical/config/__init__.py'
89--- lib/canonical/config/__init__.py 2010-01-14 16:39:18 +0000
90+++ lib/canonical/config/__init__.py 2010-04-05 20:44:34 +0000
91@@ -374,13 +374,13 @@
92 _db_config_attrs = frozenset([
93 'dbuser', 'auth_dbuser',
94 'rw_main_master', 'rw_main_slave',
95- 'ro_main_master', 'ro_main_slave', 'auth_master', 'auth_slave',
96+ 'ro_main_master', 'ro_main_slave',
97 'db_statement_timeout', 'db_statement_timeout_precision',
98 'isolation_level', 'randomise_select_results',
99 'soft_request_timeout', 'storm_cache', 'storm_cache_size'])
100 _db_config_required_attrs = frozenset([
101 'dbuser', 'rw_main_master', 'rw_main_slave', 'ro_main_master',
102- 'ro_main_slave', 'auth_master', 'auth_slave'])
103+ 'ro_main_slave'])
104
105 @property
106 def main_master(self):
107
108=== modified file 'lib/canonical/config/schema-lazr.conf'
109--- lib/canonical/config/schema-lazr.conf 2010-03-24 02:37:55 +0000
110+++ lib/canonical/config/schema-lazr.conf 2010-04-05 20:44:34 +0000
111@@ -559,8 +559,6 @@
112 rw_main_slave: dbname=launchpad_prod_2 host=chokecherry.canonical.com
113 ro_main_master: dbname=launchpad_standalone_1 host=chokecherry.canonical.com
114 ro_main_slave: dbname=launchpad_standalone_1 host=chokecherry.canonical.com
115-auth_master: dbname=launchpad_prod_3 host=wildcherry.canonical.com
116-auth_slave: dbname=launchpad_prod_2 host=chokecherry.canonical.com
117
118 # If the replication lag is more than this many seconds, slave databases
119 # will not be used.
120@@ -867,16 +865,10 @@
121 # datatype: integer
122 max_scaling: 500
123
124-[sso]
125-dbuser: sso_main
126-auth_dbuser: sso_auth
127-
128-
129 [launchpad]
130 # The database user which will be used by this process.
131 # datatype: string
132 dbuser: launchpad_main
133-auth_dbuser: launchpad_auth
134 storm_cache: generational
135 storm_cache_size: 10000
136
137
138=== modified file 'lib/canonical/configure.zcml'
139--- lib/canonical/configure.zcml 2010-01-08 21:23:15 +0000
140+++ lib/canonical/configure.zcml 2010-04-05 20:44:34 +0000
141@@ -154,7 +154,5 @@
142 <include package="canonical.lazr" />
143 <include zcml:condition="installed canonical.shipit"
144 package="canonical.shipit" />
145- <include zcml:condition="installed canonical.signon"
146- package="canonical.signon" />
147
148 </configure>
149
150=== modified file 'lib/canonical/database/harness.py'
151--- lib/canonical/database/harness.py 2009-11-05 03:52:51 +0000
152+++ lib/canonical/database/harness.py 2010-04-05 20:44:34 +0000
153@@ -48,8 +48,7 @@
154 from storm.locals import *
155 from storm.expr import *
156 from canonical.launchpad.webapp.interfaces import (
157- IStoreSelector, MAIN_STORE, AUTH_STORE, MASTER_FLAVOR,
158- SLAVE_FLAVOR, DEFAULT_FLAVOR)
159+ IStoreSelector, MAIN_STORE, MASTER_FLAVOR, SLAVE_FLAVOR, DEFAULT_FLAVOR)
160
161
162 def switch_db_user(dbuser, commit_first=True):
163
164=== modified file 'lib/canonical/database/sqlbase.py'
165--- lib/canonical/database/sqlbase.py 2010-03-23 15:04:41 +0000
166+++ lib/canonical/database/sqlbase.py 2010-04-05 20:44:34 +0000
167@@ -271,7 +271,6 @@
168 # This is only used by scripts, so we must connect to the read-write
169 # DB here -- that's why we use rw_main_master directly.
170 main_connection_string = dbconfig.rw_main_master
171- auth_connection_string = dbconfig.auth_master
172
173 # Override dbname and dbhost in the connection string if they
174 # have been passed in.
175@@ -290,7 +289,7 @@
176 match = re.search(r'host=(\S*)', main_connection_string)
177 if match is not None:
178 dbhost = match.group(1)
179- return main_connection_string, auth_connection_string, dbname, dbhost
180+ return main_connection_string, dbname, dbhost
181
182 @classmethod
183 def initZopeless(cls, dbname=None, dbhost=None, dbuser=None,
184@@ -298,7 +297,7 @@
185 # Connect to the auth master store as well, as some scripts might need
186 # to create EmailAddresses and Accounts.
187
188- main_connection_string, auth_connection_string, dbname, dbhost = (
189+ main_connection_string, dbname, dbhost = (
190 cls._get_zopeless_connection_config(dbname, dbhost))
191
192 assert dbuser is not None, '''
193@@ -315,7 +314,6 @@
194 overlay = dedent("""\
195 [database]
196 rw_main_master: %(main_connection_string)s
197- auth_master: %(auth_connection_string)s
198 isolation_level: %(isolation_level)s
199 """ % vars())
200
201
202=== modified file 'lib/canonical/database/tests/test_zopeless_transaction_manager.py'
203--- lib/canonical/database/tests/test_zopeless_transaction_manager.py 2009-12-14 17:38:34 +0000
204+++ lib/canonical/database/tests/test_zopeless_transaction_manager.py 2010-04-05 20:44:34 +0000
205@@ -1,48 +1,17 @@
206 # Copyright 2009 Canonical Ltd. This software is licensed under the
207 # GNU Affero General Public License version 3 (see the file LICENSE).
208
209-from textwrap import dedent
210 import unittest
211
212 from zope.component import getUtility
213
214 from storm.zope.interfaces import IZStorm
215
216-from canonical.config import config
217 from canonical.database.sqlbase import ZopelessTransactionManager
218 from canonical.testing.layers import LaunchpadZopelessLayer
219 from lp.testing import TestCase
220
221
222-class TestZopelessTransactionManagerNoLayer(TestCase):
223-
224- def test_initZopeless_connects_to_auth_master_db(self):
225- # Some scripts might create EmailAddress and Account entries, so
226- # initZopeless has to connect to the auth master db. This is a
227- # bugfix test. The error that this test detects is that the
228- # script used to use the main_master database for the
229- # auth_master. In this test, we make sure that the auth_master
230- # and main_master have different values in the config, and then
231- # show that they are honored. Prior to the fix, ``auth_master``
232- # would have been changed to the same value as ``main_master``.
233- # Now we set up our test data and push it on the config.
234- auth_master = "dbname=example_launchpad_auth_does_not_exist"
235- overlay = dedent("""
236- [database]
237- main_master: dbname=launchpad_dev
238- auth_master: %s
239- """ % (auth_master,))
240- config.push('new-db', overlay)
241- try:
242- main_connection_string, auth_connection_string, dbname, dbhost = (
243- ZopelessTransactionManager._get_zopeless_connection_config(
244- None, None))
245- self.assertEqual(auth_connection_string, auth_master)
246- finally:
247- # Clean up the configuration
248- config.pop('new-db')
249-
250-
251 class TestZopelessTransactionManager(TestCase):
252 layer = LaunchpadZopelessLayer
253
254
255=== modified file 'lib/canonical/launchpad/database/tests/test_oauth.py'
256--- lib/canonical/launchpad/database/tests/test_oauth.py 2009-06-25 05:30:52 +0000
257+++ lib/canonical/launchpad/database/tests/test_oauth.py 2010-04-05 20:44:34 +0000
258@@ -22,7 +22,7 @@
259 """Base tests for the OAuth database classes."""
260 layer = DatabaseFunctionalLayer
261
262- def test__get_store_should_return_the_auth_master_store(self):
263+ def test__get_store_should_return_the_main_master_store(self):
264 """We want all OAuth classes to use the master store.
265 Otherwise, the OAuth exchanges will fail because the authorize
266 screen won't probably find the new request token on the slave store.
267
268=== modified file 'lib/canonical/launchpad/doc/account.txt'
269--- lib/canonical/launchpad/doc/account.txt 2010-02-12 15:57:27 +0000
270+++ lib/canonical/launchpad/doc/account.txt 2010-04-05 20:44:34 +0000
271@@ -196,32 +196,6 @@
272 >>> account.status = AccountStatus.ACTIVE
273 >>> login('no-priv@canonical.com')
274
275-The Account's displayname is synced to the Person's displayname if there
276-is one. If the Person.displayname is changed, the Account.displayname is
277-changed too.
278-
279- >>> from canonical.launchpad.interfaces import IPersonSet
280-
281- >>> personset = getUtility(IPersonSet)
282- >>> person = personset.getByEmail('no-priv@canonical.com')
283- >>> person.displayname = 'Something New'
284- >>> print account.displayname
285- Something New
286-
287-However, the reverse is not true. If we change the Account.displayname,
288-the linked Person.displayname (if there is one) is not updated
289-immediately. Instead, a cron job will sync this information later. This
290-allows displayname changes to happen even when the Person table is
291-unavailable.
292-
293- >>> account.displayname = 'No Privileges Account'
294- >>> print person.displayname
295- Something New
296-
297- >>> person.displayname = 'No Privileges Person'
298- >>> print account.displayname
299- No Privileges Person
300-
301 An Account has an OpenID identifier used to generate the OpenID identity
302 URL.
303
304@@ -242,6 +216,7 @@
305 >>> login('admin@canonical.com')
306 >>> passwordless_account = account_set.new(
307 ... AccountCreationRationale.USER_CREATED, 'Passwordless')
308+ >>> transaction.commit()
309 >>> print passwordless_account.creation_rationale.name
310 USER_CREATED
311 >>> print passwordless_account.displayname
312
313=== modified file 'lib/canonical/launchpad/doc/storm.txt'
314--- lib/canonical/launchpad/doc/storm.txt 2010-02-22 10:33:10 +0000
315+++ lib/canonical/launchpad/doc/storm.txt 2010-04-05 20:44:34 +0000
316@@ -7,10 +7,11 @@
317 specific Storm tools to cope with our master and slave store arrangement.
318
319 >>> from canonical.launchpad.interfaces import (
320- ... EmailAddressStatus, IAccountSet, IEmailAddressSet,
321+ ... EmailAddressStatus, IEmailAddressSet,
322 ... IMasterObject, IMasterStore, ISlaveStore, IStore)
323 >>> from canonical.launchpad.database import (
324 ... Account, AccountPassword, EmailAddress)
325+ >>> from lp.registry.interfaces.person import IPersonSet
326 >>> from lp.registry.model.person import Person
327 >>> from zope.security.proxy import ProxyFactory
328
329@@ -19,30 +20,14 @@
330 a Launchpad database object. You can use adapters to
331 retrieve the correct Store.
332
333- >>> auth_master = IMasterStore(Account)
334 >>> main_master = IMasterStore(Person)
335- >>> auth_master is main_master
336- False
337-
338-
339-You can read most tables from any Store, which is required for doing
340-fast joins in the database. However, when it is not necessary to
341-retrieve objects from the same store as another object, it is better to
342-explicitly use the explicit Store for its replication set. Some tables
343-are only available from this store, such as the AccountPassword table.
344-
345- >>> auth_slave = ISlaveStore(AccountPassword)
346- >>> main_slave = ISlaveStore(Person)
347- >>> auth_slave is main_slave
348- False
349-
350
351 You can detect if a store is writable by checking what interfaces it
352 provides.
353
354- >>> IMasterStore.providedBy(auth_master)
355+ >>> IMasterStore.providedBy(main_master)
356 True
357- >>> ISlaveStore.providedBy(auth_master)
358+ >>> ISlaveStore.providedBy(main_master)
359 False
360
361
362@@ -53,7 +38,6 @@
363 Otherwise, it gives you the master. See IStoreSelector for details.
364
365 >>> main_default = IStore(Person)
366- >>> main_master = IMasterStore(Person)
367 >>> main_slave = ISlaveStore(Person)
368 >>> main_default is main_master
369 True
370@@ -78,11 +62,10 @@
371 changes to an object, just in case you have been passed an instance
372 from a store other than the correct Master.
373
374- >>> auth_slave = ISlaveStore(Account)
375+ >>> main_slave = ISlaveStore(Person)
376 >>> t = transaction.begin()
377- >>> account = auth_slave.find(
378- ... Account, openid_identifier='mark_oid').one()
379- >>> account.displayname = 'Cannot change'
380+ >>> person = main_slave.find(Person, name='mark').one()
381+ >>> person.displayname = 'Cannot change'
382 >>> transaction.commit()
383 Traceback (most recent call last):
384 ...
385@@ -90,9 +73,8 @@
386
387 >>> transaction.abort()
388 >>> t = transaction.begin()
389- >>> account = auth_slave.find(
390- ... Account, openid_identifier='mark_oid').one()
391- >>> IMasterObject(account).displayname = 'Can change'
392+ >>> person = main_slave.find(Person, name='mark').one()
393+ >>> IMasterObject(person).displayname = 'Can change'
394 >>> transaction.commit()
395
396
397@@ -100,61 +82,60 @@
398 similarly wrapped.
399
400 >>> from zope.security.proxy import removeSecurityProxy
401- >>> account = getUtility(IAccountSet).getByEmail('no-priv@canonical.com')
402- >>> removeSecurityProxy(account) is account
403- False
404- >>> account.displayname
405- u'No Privileges Person'
406- >>> account.password
407- Traceback (most recent call last):
408- ...
409- Unauthorized: ...
410-
411- >>> account = IMasterObject(account)
412- >>> removeSecurityProxy(account) is account
413- False
414- >>> account.displayname
415- u'No Privileges Person'
416- >>> account.password
417- Traceback (most recent call last):
418- ...
419- Unauthorized: ...
420-
421- >>> account = IMasterObject(removeSecurityProxy(account))
422- >>> removeSecurityProxy(account) is account
423+ >>> person = getUtility(IPersonSet).getByEmail('no-priv@canonical.com')
424+ >>> removeSecurityProxy(person) is person
425+ False
426+ >>> person.displayname
427+ u'No Privileges Person'
428+ >>> person.name = 'foo'
429+ Traceback (most recent call last):
430+ ...
431+ Unauthorized: ...
432+
433+ >>> person = IMasterObject(person)
434+ >>> removeSecurityProxy(person) is person
435+ False
436+ >>> person.displayname
437+ u'No Privileges Person'
438+ >>> person.name = 'foo'
439+ Traceback (most recent call last):
440+ ...
441+ Unauthorized: ...
442+
443+ >>> person = IMasterObject(removeSecurityProxy(person))
444+ >>> removeSecurityProxy(person) is person
445 True
446- >>> account.displayname
447+ >>> person.displayname
448 u'No Privileges Person'
449- >>> account.password
450- u'...'
451+ >>> person.name = 'foo'
452
453 Our objects may compare equal even if they have come from different
454 stores.
455
456- >>> auth_master_email = IMasterStore(EmailAddress).find(
457- ... EmailAddress, Person.name == 'janitor',
458- ... EmailAddress.person==Person.id).one()
459- >>> auth_slave_email = ISlaveStore(EmailAddress).find(
460- ... EmailAddress, Person.name == 'janitor',
461- ... EmailAddress.person==Person.id).one()
462- >>> auth_master_email is auth_slave_email
463+ >>> master_email = IMasterStore(EmailAddress).find(
464+ ... EmailAddress, Person.name == 'janitor',
465+ ... EmailAddress.person==Person.id).one()
466+ >>> slave_email = ISlaveStore(EmailAddress).find(
467+ ... EmailAddress, Person.name == 'janitor',
468+ ... EmailAddress.person==Person.id).one()
469+ >>> master_email is slave_email
470 False
471- >>> auth_master_email == auth_slave_email
472+ >>> master_email == slave_email
473 True
474- >>> auth_master_email != auth_slave_email
475+ >>> master_email != slave_email
476 False
477
478 Comparison works for security wrapped objects too.
479
480 >>> wrapped_email = getUtility(IEmailAddressSet).getByEmail(
481- ... auth_master_email.email)
482- >>> removeSecurityProxy(wrapped_email) is auth_master_email
483+ ... master_email.email)
484+ >>> removeSecurityProxy(wrapped_email) is master_email
485 True
486- >>> wrapped_email is auth_master_email
487+ >>> wrapped_email is master_email
488 False
489- >>> wrapped_email == auth_master_email
490+ >>> wrapped_email == master_email
491 True
492- >>> wrapped_email != auth_master_email
493+ >>> wrapped_email != master_email
494 False
495
496 Objects not yet flushed to the database also compare equal.
497@@ -176,10 +157,9 @@
498
499 Objects differing by class never compare equal.
500
501- >>> account_one = IMasterStore(Account).get(Account, 1)
502- >>> person_one = IMasterStore(Account).get(Person, 1)
503- >>> account_one == person_one
504+ >>> email_one = IMasterStore(EmailAddress).get(EmailAddress, 1)
505+ >>> person_one = IMasterStore(Person).get(Person, 1)
506+ >>> email_one == person_one
507 False
508- >>> account_one != person_one
509+ >>> email_one != person_one
510 True
511-
512
513=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt'
514--- lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt 2010-01-13 13:50:39 +0000
515+++ lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt 2010-04-05 20:44:34 +0000
516@@ -236,8 +236,6 @@
517 ... [database]
518 ... rw_main_master: dbname=nonexistant
519 ... rw_main_slave: dbname=nonexistant
520- ... auth_master: dbname=nonexistant
521- ... auth_slave: dbname=nonexistant
522 ...
523 ... [launchpad_session]
524 ... dbname: nonexistant
525
526=== modified file 'lib/canonical/launchpad/scripts/garbo.py'
527--- lib/canonical/launchpad/scripts/garbo.py 2010-03-26 14:33:46 +0000
528+++ lib/canonical/launchpad/scripts/garbo.py 2010-04-05 20:44:34 +0000
529@@ -29,7 +29,7 @@
530 from canonical.launchpad.utilities.looptuner import (
531 DBLoopTuner, TunableLoop)
532 from canonical.launchpad.webapp.interfaces import (
533- IStoreSelector, AUTH_STORE, MAIN_STORE, MASTER_FLAVOR)
534+ IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
535 from lp.bugs.interfaces.bug import IBugSet
536 from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource
537 from lp.bugs.model.bugnotification import BugNotification
538@@ -121,19 +121,17 @@
539 transaction.commit()
540
541
542-class OpenIDAssociationPruner(TunableLoop):
543+class OpenIDConsumerAssociationPruner(TunableLoop):
544 minimum_chunk_size = 3500
545 maximum_chunk_size = 50000
546
547- table_name = 'OpenIDAssociation'
548- store_name = AUTH_STORE
549+ table_name = 'OpenIDConsumerAssociation'
550
551 _num_removed = None
552
553 def __init__(self, log, abort_time=None):
554- super(OpenIDAssociationPruner, self).__init__(log, abort_time)
555- self.store = getUtility(IStoreSelector).get(
556- self.store_name, MASTER_FLAVOR)
557+ super(OpenIDConsumerAssociationPruner, self).__init__(log, abort_time)
558+ self.store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
559
560 def __call__(self, chunksize):
561 result = self.store.execute("""
562@@ -152,11 +150,6 @@
563 return self._num_removed == 0
564
565
566-class OpenIDConsumerAssociationPruner(OpenIDAssociationPruner):
567- table_name = 'OpenIDConsumerAssociation'
568- store_name = MAIN_STORE
569-
570-
571 class RevisionCachePruner(TunableLoop):
572 """A tunable loop to remove old revisions from the cache."""
573
574@@ -869,7 +862,6 @@
575 tunable_loops = [
576 OAuthNoncePruner,
577 OpenIDConsumerNoncePruner,
578- OpenIDAssociationPruner,
579 OpenIDConsumerAssociationPruner,
580 RevisionCachePruner,
581 BugHeatUpdater,
582
583=== modified file 'lib/canonical/launchpad/scripts/tests/test_garbo.py'
584--- lib/canonical/launchpad/scripts/tests/test_garbo.py 2010-01-22 06:03:19 +0000
585+++ lib/canonical/launchpad/scripts/tests/test_garbo.py 2010-04-05 20:44:34 +0000
586@@ -29,11 +29,11 @@
587 from lp.testing import TestCase, TestCaseWithFactory
588 from canonical.launchpad.scripts.garbo import (
589 DailyDatabaseGarbageCollector, HourlyDatabaseGarbageCollector,
590- OpenIDAssociationPruner, OpenIDConsumerAssociationPruner)
591+ OpenIDConsumerAssociationPruner)
592 from canonical.launchpad.scripts.tests import run_script
593 from canonical.launchpad.scripts.logger import QuietFakeLogger
594 from canonical.launchpad.webapp.interfaces import (
595- IStoreSelector, MASTER_FLAVOR)
596+ IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
597 from canonical.testing.layers import (
598 DatabaseLayer, LaunchpadScriptLayer, LaunchpadZopelessLayer)
599 from lp.bugs.model.bugnotification import (
600@@ -219,12 +219,12 @@
601 Min(CodeImportResult.date_created)).one().replace(tzinfo=UTC)
602 >= now - timedelta(days=30))
603
604- def test_OpenIDAssociationPruner(self, pruner=OpenIDAssociationPruner):
605- store_name = pruner.store_name
606+ def test_OpenIDConsumerAssociationPruner(self):
607+ pruner = OpenIDConsumerAssociationPruner
608 table_name = pruner.table_name
609 LaunchpadZopelessLayer.switchDbUser('testadmin')
610 store_selector = getUtility(IStoreSelector)
611- store = store_selector.get(store_name, MASTER_FLAVOR)
612+ store = store_selector.get(MAIN_STORE, MASTER_FLAVOR)
613 now = time.time()
614 # Create some associations in the past with lifetimes
615 for delta in range(0, 20):
616@@ -247,7 +247,7 @@
617 self.runHourly()
618
619 LaunchpadZopelessLayer.switchDbUser('testadmin')
620- store = store_selector.get(store_name, MASTER_FLAVOR)
621+ store = store_selector.get(MAIN_STORE, MASTER_FLAVOR)
622 # Confirm all the rows we know should have been expired have
623 # been expired. These are the ones that would be expired using
624 # the test start time as 'now'.
625@@ -263,9 +263,6 @@
626 "SELECT COUNT(*) FROM %s" % table_name).get_one()[0]
627 self.failUnless(num_unexpired > 0)
628
629- def test_OpenIDConsumerAssociationPruner(self):
630- self.test_OpenIDAssociationPruner(OpenIDConsumerAssociationPruner)
631-
632 def test_RevisionAuthorEmailLinker(self):
633 LaunchpadZopelessLayer.switchDbUser('testadmin')
634 rev1 = self.factory.makeRevision('Author 1 <author-1@Example.Org>')
635@@ -373,6 +370,7 @@
636
637 # If we remove the email address that was subscribed, the
638 # garbage collector removes the subscription.
639+ LaunchpadZopelessLayer.switchDbUser('testadmin')
640 Store.of(email).remove(email)
641 transaction.commit()
642 self.runDaily()
643
644=== modified file 'lib/canonical/launchpad/webapp/adapter.py'
645--- lib/canonical/launchpad/webapp/adapter.py 2010-03-25 11:58:42 +0000
646+++ lib/canonical/launchpad/webapp/adapter.py 2010-04-05 20:44:34 +0000
647@@ -40,14 +40,13 @@
648 from canonical.launchpad.readonly import is_read_only
649 from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy
650 from canonical.launchpad.webapp.interfaces import (
651- AUTH_STORE, DEFAULT_FLAVOR, IStoreSelector,
652- MAIN_STORE, MASTER_FLAVOR, ReadOnlyModeViolation, SLAVE_FLAVOR)
653+ DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE, MASTER_FLAVOR,
654+ ReadOnlyModeViolation, SLAVE_FLAVOR)
655 from canonical.launchpad.webapp.opstats import OpStats
656 from canonical.lazr.utils import safe_hasattr
657
658
659 __all__ = [
660- 'DisconnectionError',
661 'RequestExpired',
662 'set_request_started',
663 'clear_request_started',
664@@ -327,7 +326,7 @@
665
666 def __init__(self, uri):
667 # The uri is just a property name in the config, such as main_master
668- # or auth_slave.
669+ # or main_slave.
670 # We don't invoke the superclass constructor as it has a very limited
671 # opinion on what uri is.
672 # pylint: disable-msg=W0231
673@@ -356,7 +355,7 @@
674 'Connection uri %s does not match section-realm-flavor format'
675 % repr(self._uri.database))
676
677- assert realm in ('main', 'auth'), 'Unknown realm %s' % realm
678+ assert realm == 'main', 'Unknown realm %s' % realm
679 assert flavor in ('master', 'slave'), 'Unknown flavor %s' % flavor
680
681 my_dbconfig = DatabaseConfig()
682@@ -572,14 +571,6 @@
683 return db_policy.getStore(name, flavor)
684
685
686-# There are not many tables outside of the main replication set, so we
687-# can just maintain a hardcoded list of what isn't in there for now.
688-_auth_store_tables = frozenset([
689- 'Account', 'AccountPassword', 'AuthToken', 'EmailAddress',
690- 'OpenIDAssociation', 'OpenIDAuthorization', 'OpenIDNonce',
691- 'OpenIDRPSummary'])
692-
693-
694 # We want to be able to adapt a Storm class to an IStore, IMasterStore or
695 # ISlaveStore. Unfortunately, the component architecture provides no
696 # way for us to declare that a class, and all its subclasses, provides
697@@ -588,9 +579,7 @@
698 def get_store(storm_class, flavor=DEFAULT_FLAVOR):
699 """Return a flavored Store for the given database class."""
700 table = getattr(removeSecurityProxy(storm_class), '__storm_table__', None)
701- if table in _auth_store_tables:
702- return getUtility(IStoreSelector).get(AUTH_STORE, flavor)
703- elif table is not None:
704+ if table is not None:
705 return getUtility(IStoreSelector).get(MAIN_STORE, flavor)
706 else:
707 return None
708
709=== modified file 'lib/canonical/launchpad/webapp/ftests/test_adapter_permissions.txt'
710--- lib/canonical/launchpad/webapp/ftests/test_adapter_permissions.txt 2009-06-11 01:28:55 +0000
711+++ lib/canonical/launchpad/webapp/ftests/test_adapter_permissions.txt 2010-04-05 20:44:34 +0000
712@@ -10,8 +10,7 @@
713
714 >>> from lp.registry.model.person import Person
715 >>> from canonical.launchpad.webapp.interfaces import (
716- ... IStoreSelector, AUTH_STORE, MAIN_STORE,
717- ... MASTER_FLAVOR, SLAVE_FLAVOR)
718+ ... IStoreSelector, MAIN_STORE, MASTER_FLAVOR, SLAVE_FLAVOR)
719 >>> import transaction
720 >>> from zope.component import getUtility
721
722@@ -47,25 +46,3 @@
723 >>> main_master.find(Person, name='janitor').one().displayname
724 u'BenD'
725 >>> transaction.abort()
726-
727-
728-A MASTER_FLAVOR Store does not allow writes to tables outside of that
729-Store's replication set.
730-
731- >>> t = transaction.begin()
732- >>> person = main_master.find(Person, name='no-priv').one()
733- >>> account = person.account
734- >>> account.displayname = "Ben Dover"
735- >>> main_master.flush()
736- Traceback (most recent call last):
737- ...
738- ProgrammingError: permission denied for relation account
739- >>> transaction.abort()
740-
741- >>> t = transaction.begin()
742- >>> auth_master = getUtility(IStoreSelector).get(AUTH_STORE, MASTER_FLAVOR)
743- >>> person = auth_master.find(Person, name='no-priv').one()
744- >>> account = person.account
745- >>> account.displayname = "Ben Dover"
746- >>> auth_master.flush()
747- >>> transaction.abort()
748
749=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
750--- lib/canonical/launchpad/webapp/interfaces.py 2010-03-26 22:57:38 +0000
751+++ lib/canonical/launchpad/webapp/interfaces.py 2010-04-05 20:44:34 +0000
752@@ -745,9 +745,7 @@
753 #
754
755 MAIN_STORE = 'main' # The main database.
756-AUTH_STORE = 'auth' # The authentication database.
757-
758-ALL_STORES = frozenset([MAIN_STORE, AUTH_STORE])
759+ALL_STORES = frozenset([MAIN_STORE])
760
761 DEFAULT_FLAVOR = 'default' # Default flavor for current state.
762 MASTER_FLAVOR = 'master' # The master database.
763
764=== modified file 'lib/canonical/launchpad/webapp/tests/test_dbpolicy.py'
765--- lib/canonical/launchpad/webapp/tests/test_dbpolicy.py 2010-02-24 23:18:40 +0000
766+++ lib/canonical/launchpad/webapp/tests/test_dbpolicy.py 2010-04-05 20:44:34 +0000
767@@ -25,7 +25,7 @@
768 ReadOnlyLaunchpadDatabasePolicy, SlaveDatabasePolicy,
769 SlaveOnlyDatabasePolicy)
770 from canonical.launchpad.webapp.interfaces import (
771- ALL_STORES, AUTH_STORE, DEFAULT_FLAVOR, DisallowedStore, IDatabasePolicy,
772+ ALL_STORES, DEFAULT_FLAVOR, DisallowedStore, IDatabasePolicy,
773 IStoreSelector, MAIN_STORE, MASTER_FLAVOR, ReadOnlyModeDisallowedStore,
774 SLAVE_FLAVOR)
775 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
776@@ -47,9 +47,6 @@
777 main_store = store_selector.get(MAIN_STORE, DEFAULT_FLAVOR)
778 self.failUnlessEqual(self.getDBUser(main_store), 'launchpad_main')
779
780- auth_store = store_selector.get(AUTH_STORE, DEFAULT_FLAVOR)
781- self.failUnlessEqual(self.getDBUser(auth_store), 'launchpad_auth')
782-
783 def getDBUser(self, store):
784 return store.execute(
785 'SHOW session_authorization').get_one()[0]
786
787=== removed symlink 'lib/canonical/signon'
788=== target was u'../../sourcecode/canonical-identity-provider'
789=== modified file 'lib/lp/bugs/doc/checkwatches.txt'
790--- lib/lp/bugs/doc/checkwatches.txt 2010-03-26 15:24:59 +0000
791+++ lib/lp/bugs/doc/checkwatches.txt 2010-04-05 20:44:34 +0000
792@@ -337,9 +337,13 @@
793 If a bug tracker doesn't have any watches to update, forceUpdateAll()
794 will ignore it.
795
796+ >>> transaction.commit()
797+ >>> LaunchpadZopelessLayer.switchDbUser('launchpad')
798 >>> login('test@canonical.com')
799 >>> empty_tracker = factory.makeBugTracker(
800 ... 'http://example.com', BugTrackerType.ROUNDUP)
801+ >>> transaction.commit()
802+ >>> LaunchpadZopelessLayer.switchDbUser(config.checkwatches.dbuser)
803 >>> empty_tracker_name = empty_tracker.name
804 >>> update_all(empty_tracker_name)
805 INFO Bug tracker 'auto-example.com' doesn't have any watches. Ignoring.
806
807=== modified file 'lib/lp/bugs/doc/externalbugtracker-comment-imports.txt'
808--- lib/lp/bugs/doc/externalbugtracker-comment-imports.txt 2010-03-25 14:28:33 +0000
809+++ lib/lp/bugs/doc/externalbugtracker-comment-imports.txt 2010-04-05 20:44:34 +0000
810@@ -373,8 +373,10 @@
811 since they aren't a valid Launchpad user, having been created during the
812 import process.
813
814-We'll add a listener to check for Karma events.
815+We'll create a bug watch and add a listener to check for Karma events.
816
817+ >>> LaunchpadZopelessLayer.switchDbUser('launchpad')
818+ >>> bug_watch = factory.makeBugWatch('123456')
819 >>> from lp.registry.tests.karma import KarmaAssignedEventListener
820 >>> karma_helper = KarmaAssignedEventListener()
821 >>> karma_helper.register_listener()
822@@ -382,8 +384,6 @@
823 Importing a comment with a CVE reference will produce a CVE link in
824 Launchpad but will result in no Karma records being created.
825
826- >>> LaunchpadZopelessLayer.switchDbUser('launchpad')
827- >>> bug_watch = factory.makeBugWatch('123456')
828 >>> transaction.commit()
829 >>> LaunchpadZopelessLayer.switchDbUser(config.checkwatches.dbuser)
830 >>> external_bugtracker.remote_comments = {
831
832=== modified file 'lib/lp/bugs/interfaces/bug.py'
833--- lib/lp/bugs/interfaces/bug.py 2010-03-12 06:24:59 +0000
834+++ lib/lp/bugs/interfaces/bug.py 2010-04-05 20:44:34 +0000
835@@ -288,9 +288,9 @@
836 number_of_duplicates = exported(
837 Int(title=_('The number of bugs marked as duplicates of this bug'),
838 required=True, readonly=True))
839- message_count = Int(
840- title=_('The number of comments on this bug'),
841- required=True, readonly=True)
842+ message_count = exported(
843+ Int(title=_('The number of comments on this bug'),
844+ required=True, readonly=True))
845 users_affected_count = exported(
846 Int(title=_('The number of users affected by this bug '
847 '(not including duplicates)'),
848
849=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
850--- lib/lp/bugs/stories/webservice/xx-bug.txt 2010-03-24 21:59:44 +0000
851+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2010-04-05 20:44:34 +0000
852@@ -34,6 +34,7 @@
853 id: 11
854 latest_patch_uploaded: None
855 linked_branches_collection_link: u'http://.../bugs/11/linked_branches'
856+ message_count: 7
857 messages_collection_link: u'http://.../bugs/11/messages'
858 name: None
859 number_of_duplicates: 0
860
861=== modified file 'lib/lp/code/doc/branch-karma.txt'
862--- lib/lp/code/doc/branch-karma.txt 2010-02-16 20:36:48 +0000
863+++ lib/lp/code/doc/branch-karma.txt 2010-04-05 20:44:34 +0000
864@@ -49,6 +49,7 @@
865 You get karma for linking a bug to a branch.
866
867 >>> bug = factory.makeBug(product=fooix)
868+ Karma added: action=bugcreated, product=fooix, person=person-name11
869 >>> branch_link = bug.linkBranch(branch, eric)
870 Karma added: action=bugbranchcreated, product=fooix, person=eric
871
872
873=== modified file 'lib/lp/registry/configure.zcml'
874--- lib/lp/registry/configure.zcml 2010-03-19 11:33:44 +0000
875+++ lib/lp/registry/configure.zcml 2010-04-05 20:44:34 +0000
876@@ -1644,18 +1644,60 @@
877 <!-- DistributionMirror -->
878 <class class="lp.registry.model.distributionmirror.DistributionMirror">
879 <allow
880- interface="lp.registry.interfaces.distributionmirror.IDistributionMirrorPublic" />
881+ attributes="
882+ id
883+ name
884+ displayname
885+ description
886+ distribution
887+ http_base_url
888+ ftp_base_url
889+ rsync_base_url
890+ enabled
891+ speed
892+ status
893+ country
894+ content
895+ owner
896+ title
897+ cdimage_series
898+ source_series
899+ arch_series
900+ last_probe_record
901+ all_probe_records
902+ has_ftp_or_rsync_base_url
903+ base_url
904+ date_created
905+ country_dns_mirror
906+ mirrorMustHaveHTTPOrFTPURL
907+ getSummarizedMirroredSourceSeries
908+ getSummarizedMirroredArchSeries
909+ getOverallFreshness
910+ isOfficial
911+ shouldDisable
912+ disable
913+ newProbeRecord
914+ deleteMirrorDistroArchSeries
915+ ensureMirrorDistroArchSeries
916+ ensureMirrorDistroSeriesSource
917+ deleteMirrorDistroSeriesSource
918+ ensureMirrorCDImageSeries
919+ deleteMirrorCDImageSeries
920+ deleteAllMirrorCDImageSeries
921+ getExpectedPackagesPaths
922+ getExpectedSourcesPaths
923+ canTransitionToCountryMirror" />
924 <require
925 permission="launchpad.Edit"
926- interface="lp.registry.interfaces.distributionmirror.IDistributionMirrorEditRestricted"
927 set_attributes="name displayname description whiteboard
928 http_base_url ftp_base_url rsync_base_url enabled
929- speed country content official_candidate owner" />
930+ speed country content official_candidate owner"
931+ attributes="official_candidate whiteboard" />
932 <require
933 permission="launchpad.Admin"
934- interface="lp.registry.interfaces.distributionmirror.IDistributionMirrorAdminRestricted"
935 set_attributes="status reviewer date_reviewed"
936- attributes="destroySelf" />
937+ attributes="reviewer date_reviewed destroySelf
938+ transitionToCountryMirror" />
939 </class>
940
941 <adapter
942
943=== modified file 'lib/lp/registry/doc/distribution-mirror.txt'
944--- lib/lp/registry/doc/distribution-mirror.txt 2009-12-09 10:03:20 +0000
945+++ lib/lp/registry/doc/distribution-mirror.txt 2010-04-05 20:44:34 +0000
946@@ -8,7 +8,7 @@
947 >>> from canonical.launchpad.interfaces import (
948 ... ICountrySet, IDistributionSet, IDistributionMirrorSet,
949 ... IDistroArchSeriesSet, IDistroSeriesSet, ILibraryFileAliasSet,
950- ... IPersonSet, MirrorContent, MirrorSpeed)
951+ ... IPersonSet, MirrorContent, MirrorSpeed, MirrorStatus)
952 >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
953 >>> mirrorset = getUtility(IDistributionMirrorSet)
954 >>> distroset = getUtility(IDistributionSet)
955@@ -814,3 +814,121 @@
956 >>> mirrorset.getByName('invalid-mirror') is None
957 True
958
959+Country DNS mirrors
960+-------------------
961+
962+Country DNS mirrors are mirrors which have been assigned
963+$CC.archive.ubuntu.com or $CC.releases.ubuntu.com. These assignments are
964+tracked in Launchpad.
965+
966+ >>> login('admin@canonical.com')
967+ >>> ubuntu_distro = getUtility(IDistributionSet).getByName('ubuntu')
968+ >>> de_archive_mirror = factory.makeMirror(ubuntu_distro,
969+ ... "Technische Universitaet Dresden", country=82,
970+ ... http_url="http://ubuntu.mirror.tudos.de/ubuntu/",
971+ ... official_candidate=True)
972+ >>> davis_station_archive = factory.makeMirror(ubuntu_distro,
973+ ... "Davis Station", country=9,
974+ ... http_url="http://mirror.davis.antarctica.org/ubuntu",
975+ ... official_candidate=True)
976+ >>> de_archive_mirror.status = MirrorStatus.OFFICIAL
977+ >>> de_archive_prober_log = factory.makeMirrorProbeRecord(de_archive_mirror)
978+ >>> logout()
979+
980+Normal users can access country_dns_mirror, can see if a mirror is eligible
981+for the status, however, they may not change it:
982+
983+ >>> login('test@canonical.com')
984+ >>> de_archive_mirror.canTransitionToCountryMirror()
985+ True
986+ >>> de_archive_mirror.transitionToCountryMirror(True)
987+ Traceback (most recent call last):
988+ ...
989+ Unauthorized: (<DistributionMirror at ...>, 'transitionToCountryMirror',
990+ 'launchpad.Admin')
991+ >>> logout()
992+
993+Mirror listing administrators may change the status however:
994+
995+ >>> login('karl@canonical.com')
996+ >>> de_archive_mirror.transitionToCountryMirror(True)
997+
998+Mirrors which are already set as country mirrors can't be 'set' as such
999+again:
1000+
1001+ >>> de_archive_mirror.canTransitionToCountryMirror()
1002+ False
1003+ >>> de_archive_mirror.transitionToCountryMirror(True)
1004+ >>> logout()
1005+
1006+There cannot be multiple country mirrors of one type for one country:
1007+
1008+ >>> login('karl@canonical.com')
1009+ >>> proberecord = factory.makeMirrorProbeRecord(davis_station_archive)
1010+
1011+ >>> print davis_station_archive.content.name
1012+ ARCHIVE
1013+ >>> print davis_station_archive.country_dns_mirror
1014+ False
1015+ >>> print davis_station_archive.country.name
1016+ Antarctica
1017+
1018+ >>> archive_mirror2 = getUtility(IDistributionMirrorSet).getByName(
1019+ ... 'archive-mirror2')
1020+ >>> print archive_mirror2.content.name
1021+ ARCHIVE
1022+ >>> print archive_mirror2.country_dns_mirror
1023+ False
1024+ >>> print archive_mirror2.country.name
1025+ Antarctica
1026+
1027+ >>> davis_station_archive.status = MirrorStatus.OFFICIAL
1028+
1029+ >>> davis_station_archive.transitionToCountryMirror(True)
1030+ >>> archive_mirror2.transitionToCountryMirror(True)
1031+ Traceback (most recent call last):
1032+ ...
1033+ CountryMirrorAlreadySet: Antarctica already has a country Archive mirror
1034+ set.
1035+
1036+Mirrors which have not been probed may not be marked as country mirrors:
1037+
1038+ >>> linux_au_mirror = factory.makeMirror(ubuntu_distro,
1039+ ... "Linux.org.au", country=14,
1040+ ... http_url="http://mirror.linux.org.au/ubuntu",
1041+ ... official_candidate=True)
1042+ >>> linux_au_mirror.status = MirrorStatus.OFFICIAL
1043+ >>> linux_au_mirror.transitionToCountryMirror(True)
1044+ Traceback (most recent call last):
1045+ ...
1046+ MirrorNotProbed: This mirror may not be set as a country mirror as it has
1047+ not been probed.
1048+ >>> logout()
1049+
1050+Mirrors which are not official or do not have an HTTP URL may not be set as
1051+country mirrors:
1052+
1053+ >>> login('admin@canonical.com')
1054+ >>> osuosl_mirror = factory.makeMirror(ubuntu_distro,
1055+ ... "OSU Open Source Lab", country=226,
1056+ ... ftp_url="ftp://ubuntu.osuosl.org/pub/ubuntu/",
1057+ ... official_candidate=True)
1058+ >>> osuosl_mirror.status = MirrorStatus.OFFICIAL
1059+ >>> print osuosl_mirror.http_base_url
1060+ None
1061+
1062+ >>> osuosl_mirror.canTransitionToCountryMirror()
1063+ False
1064+
1065+ >>> osuosl_mirror.transitionToCountryMirror(None)
1066+ Traceback (most recent call last):
1067+ ...
1068+ NoneError: None isn't acceptable as a value for
1069+ DistributionMirror.country_dns_mirror
1070+
1071+ >>> osuosl_mirror.transitionToCountryMirror(True)
1072+ Traceback (most recent call last):
1073+ ...
1074+ MirrorHasNoHTTPURL: This mirror may not be set as a country mirror as it
1075+ does not have an HTTP URL set.
1076+ >>> logout()
1077
1078=== modified file 'lib/lp/registry/interfaces/distribution.py'
1079--- lib/lp/registry/interfaces/distribution.py 2010-03-24 21:59:58 +0000
1080+++ lib/lp/registry/interfaces/distribution.py 2010-04-05 20:44:34 +0000
1081@@ -25,7 +25,7 @@
1082
1083 from lazr.restful.fields import CollectionField, Reference
1084 from lazr.restful.declarations import (
1085- collection_default_content, export_as_webservice_collection,
1086+ collection_default_content, copy_field, export_as_webservice_collection,
1087 export_as_webservice_entry, export_operation_as,
1088 export_read_operation, exported, operation_parameters,
1089 operation_returns_collection_of, operation_returns_entry,
1090@@ -321,6 +321,14 @@
1091 if it's not found.
1092 """
1093
1094+ @operation_parameters(
1095+ country=copy_field(IDistributionMirror['country'], required=True),
1096+ mirror_type=copy_field(IDistributionMirror['content'], required=True))
1097+ @operation_returns_entry(IDistributionMirror)
1098+ @export_read_operation()
1099+ def getCountryMirror(country, mirror_type):
1100+ """Return the country DNS mirror for a country and content type."""
1101+
1102 def newMirror(owner, speed, country, content, displayname=None,
1103 description=None, http_base_url=None,
1104 ftp_base_url=None, rsync_base_url=None, enabled=False,
1105
1106=== modified file 'lib/lp/registry/interfaces/distributionmirror.py'
1107--- lib/lp/registry/interfaces/distributionmirror.py 2010-02-22 15:50:06 +0000
1108+++ lib/lp/registry/interfaces/distributionmirror.py 2010-04-05 20:44:34 +0000
1109@@ -6,21 +6,24 @@
1110 __metaclass__ = type
1111
1112 __all__ = [
1113-'IDistributionMirror',
1114-'IDistributionMirrorAdminRestricted',
1115-'IDistributionMirrorEditRestricted',
1116-'IDistributionMirrorPublic',
1117-'IMirrorDistroArchSeries',
1118-'IMirrorDistroSeriesSource',
1119-'IMirrorProbeRecord',
1120-'IDistributionMirrorSet',
1121-'IMirrorCDImageDistroSeries',
1122-'PROBE_INTERVAL',
1123-'UnableToFetchCDImageFileList',
1124-'MirrorContent',
1125-'MirrorFreshness',
1126-'MirrorSpeed',
1127-'MirrorStatus']
1128+ 'CannotTransitionToCountryMirror',
1129+ 'CountryMirrorAlreadySet',
1130+ 'IDistributionMirror',
1131+ 'IMirrorDistroArchSeries',
1132+ 'IMirrorDistroSeriesSource',
1133+ 'IMirrorProbeRecord',
1134+ 'IDistributionMirrorSet',
1135+ 'IMirrorCDImageDistroSeries',
1136+ 'PROBE_INTERVAL',
1137+ 'MirrorContent',
1138+ 'MirrorFreshness',
1139+ 'MirrorHasNoHTTPURL',
1140+ 'MirrorNotOfficial',
1141+ 'MirrorNotProbed',
1142+ 'MirrorSpeed',
1143+ 'MirrorStatus',
1144+ 'UnableToFetchCDImageFileList',
1145+ ]
1146
1147 from cgi import escape
1148
1149@@ -31,8 +34,11 @@
1150 from zope.component import getUtility
1151 from lazr.enum import DBEnumeratedType, DBItem
1152 from lazr.restful.declarations import (
1153- export_as_webservice_entry, export_read_operation, exported)
1154+ export_as_webservice_entry, export_read_operation,
1155+ export_write_operation, exported, mutator_for, operation_parameters,
1156+ webservice_error)
1157 from lazr.restful.fields import Reference, ReferenceChoice
1158+from lazr.restful.interface import copy_field
1159
1160 from canonical.launchpad import _
1161 from canonical.launchpad.fields import (
1162@@ -47,6 +53,43 @@
1163 PROBE_INTERVAL = 23
1164
1165
1166+class CannotTransitionToCountryMirror(Exception):
1167+ """Root exception for transitions to country mirrors."""
1168+ webservice_error(400)
1169+
1170+
1171+class CountryMirrorAlreadySet(CannotTransitionToCountryMirror):
1172+ """Distribution mirror cannot be set as a country mirror.
1173+
1174+ Raised when a user tries to change set a distribution mirror as a country
1175+ mirror, however there is already one set for that country.
1176+ """
1177+
1178+
1179+class MirrorNotOfficial(CannotTransitionToCountryMirror):
1180+ """Distribution mirror is not permitted to become a country mirror.
1181+
1182+ Raised when a user tries to change set a distribution mirror as a country
1183+ mirror, however the mirror in question is not official.
1184+ """
1185+
1186+
1187+class MirrorHasNoHTTPURL(CannotTransitionToCountryMirror):
1188+ """Distribution mirror has no HTTP URL.
1189+
1190+ Raised when a user tries to make an official mirror a country mirror,
1191+ however the mirror has not HTTP URL set.
1192+ """
1193+
1194+
1195+class MirrorNotProbed(CannotTransitionToCountryMirror):
1196+ """Distribution mirror has not been probed.
1197+
1198+ Raised when a user tries to set an official mirror as a country mirror,
1199+ however the mirror has not been probed yet.
1200+ """
1201+
1202+
1203 class MirrorContent(DBEnumeratedType):
1204 """The content that is mirrored."""
1205
1206@@ -284,33 +327,10 @@
1207 def getMirrorByURI(self, url):
1208 return getUtility(IDistributionMirrorSet).getByRsyncUrl(url)
1209
1210-class IDistributionMirrorAdminRestricted(Interface):
1211- """IDistributionMirror properties requiring launchpad.Admin permission."""
1212-
1213- reviewer = exported(PublicPersonChoice(
1214- title=_('Reviewer'), required=False, readonly=True,
1215- vocabulary='ValidPersonOrTeam', description=_(
1216- "The person who last reviewed this mirror.")))
1217- date_reviewed = exported(Datetime(
1218- title=_('Date reviewed'), required=False, readonly=True,
1219- description=_(
1220- "The date on which this mirror was last reviewed by a mirror admin.")))
1221-
1222-
1223-class IDistributionMirrorEditRestricted(Interface):
1224- """IDistributionMirror properties requiring launchpad.Edit permission."""
1225-
1226- official_candidate = exported(Bool(
1227- title=_('Apply to be an official mirror of this distribution'),
1228- required=False, readonly=False, default=True))
1229- whiteboard = exported(Whiteboard(
1230- title=_('Whiteboard'), required=False, readonly=False,
1231- description=_("Notes on the current status of the mirror (only "
1232- "visible to admins and the mirror's registrant).")))
1233-
1234-
1235-class IDistributionMirrorPublic(Interface):
1236- """Public IDistributionMirror properties."""
1237+
1238+class IDistributionMirror(Interface):
1239+ """A mirror of a given distribution."""
1240+ export_as_webservice_entry()
1241
1242 id = Int(title=_('The unique id'), required=True, readonly=True)
1243 owner = exported(PublicPersonChoice(
1244@@ -386,6 +406,39 @@
1245 date_created = exported(Datetime(
1246 title=_('Date Created'), required=True, readonly=True,
1247 description=_("The date on which this mirror was registered.")))
1248+ country_dns_mirror = exported(Bool(
1249+ title=_('Country DNS Mirror'),
1250+ description=_('Whether this is a country mirror in DNS.'),
1251+ required=False, readonly=True, default=False))
1252+
1253+ reviewer = exported(PublicPersonChoice(
1254+ title=_('Reviewer'), required=False, readonly=True,
1255+ vocabulary='ValidPersonOrTeam', description=_(
1256+ "The person who last reviewed this mirror.")))
1257+ date_reviewed = exported(Datetime(
1258+ title=_('Date reviewed'), required=False, readonly=True,
1259+ description=_(
1260+ "The date on which this mirror was last reviewed by a mirror "
1261+ "admin.")))
1262+
1263+ official_candidate = exported(Bool(
1264+ title=_('Apply to be an official mirror of this distribution'),
1265+ required=False, readonly=False, default=True))
1266+ whiteboard = exported(Whiteboard(
1267+ title=_('Whiteboard'), required=False, readonly=False,
1268+ description=_("Notes on the current status of the mirror (only "
1269+ "visible to admins and the mirror's registrant).")))
1270+
1271+ @export_read_operation()
1272+ def canTransitionToCountryMirror():
1273+ """Verify if a mirror can be set as a country mirror or return
1274+ False."""
1275+
1276+ @mutator_for(country_dns_mirror)
1277+ @operation_parameters(country_dns_mirror=copy_field(country_dns_mirror))
1278+ @export_write_operation()
1279+ def transitionToCountryMirror(country_dns_mirror):
1280+ """Method run on changing country_dns_mirror."""
1281
1282 @invariant
1283 def mirrorMustHaveHTTPOrFTPURL(mirror):
1284@@ -521,10 +574,6 @@
1285 Sources.gz file refer to and the path to the file itself.
1286 """
1287
1288-class IDistributionMirror(IDistributionMirrorAdminRestricted,
1289- IDistributionMirrorEditRestricted, IDistributionMirrorPublic):
1290- """A mirror of a given distribution."""
1291- export_as_webservice_entry()
1292
1293
1294 class UnableToFetchCDImageFileList(Exception):
1295
1296=== modified file 'lib/lp/registry/model/distribution.py'
1297--- lib/lp/registry/model/distribution.py 2010-03-24 02:53:42 +0000
1298+++ lib/lp/registry/model/distribution.py 2010-04-05 20:44:34 +0000
1299@@ -402,6 +402,17 @@
1300 """See `IDistribution`."""
1301 return DistributionMirror.selectOneBy(distribution=self, name=name)
1302
1303+ def getCountryMirror(self, country, mirror_type):
1304+ """See `IDistribution`."""
1305+ store = Store.of(self)
1306+ results = store.find(
1307+ DistributionMirror,
1308+ DistributionMirror.distribution == self,
1309+ DistributionMirror.country == country,
1310+ DistributionMirror.content == mirror_type,
1311+ DistributionMirror.country_dns_mirror == True)
1312+ return results.one()
1313+
1314 def newMirror(self, owner, speed, country, content, displayname=None,
1315 description=None, http_base_url=None,
1316 ftp_base_url=None, rsync_base_url=None,
1317
1318=== modified file 'lib/lp/registry/model/distributionmirror.py'
1319--- lib/lp/registry/model/distributionmirror.py 2010-03-23 20:42:23 +0000
1320+++ lib/lp/registry/model/distributionmirror.py 2010-04-05 20:44:34 +0000
1321@@ -45,9 +45,11 @@
1322 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
1323 from lp.soyuz.interfaces.publishing import PackagePublishingStatus
1324 from lp.registry.interfaces.distributionmirror import (
1325+ CannotTransitionToCountryMirror, CountryMirrorAlreadySet,
1326 IDistributionMirror, IDistributionMirrorSet, IMirrorCDImageDistroSeries,
1327 IMirrorDistroArchSeries, IMirrorDistroSeriesSource, IMirrorProbeRecord,
1328- MirrorContent, MirrorFreshness, MirrorSpeed, MirrorStatus, PROBE_INTERVAL)
1329+ MirrorContent, MirrorFreshness, MirrorHasNoHTTPURL, MirrorNotOfficial,
1330+ MirrorNotProbed, MirrorSpeed, MirrorStatus, PROBE_INTERVAL)
1331 from lp.registry.interfaces.distroseries import IDistroSeries
1332 from lp.registry.interfaces.sourcepackage import SourcePackageFileType
1333 from canonical.launchpad.mail import simple_sendmail, format_address
1334@@ -100,6 +102,8 @@
1335 date_reviewed = UtcDateTimeCol(default=None)
1336 whiteboard = StringCol(
1337 notNull=False, default=None)
1338+ country_dns_mirror = BoolCol(
1339+ notNull=True, default=False)
1340
1341 @property
1342 def base_url(self):
1343@@ -145,6 +149,64 @@
1344 "This mirror has been probed and thus can't be removed.")
1345 SQLBase.destroySelf(self)
1346
1347+ def verifyTransitionToCountryMirror(self):
1348+ """Verify that a mirror can be set as a country mirror.
1349+
1350+ Return True if valid, otherwise raise a subclass of
1351+ CannotTransitionToCountryMirror.
1352+ """
1353+
1354+ current_country_mirror = self.distribution.getCountryMirror(
1355+ self.country, self.content)
1356+
1357+ if current_country_mirror is not None:
1358+ # Country already has a country mirror.
1359+ raise CountryMirrorAlreadySet(
1360+ "%s already has a country %s mirror set." % (
1361+ self.country.name, self.content))
1362+
1363+ if not self.isOfficial():
1364+ # Only official mirrors may be set as country mirrors.
1365+ raise MirrorNotOfficial(
1366+ "This mirror may not be set as a country mirror as it is not "
1367+ "an official mirror.")
1368+
1369+ if self.http_base_url is None:
1370+ # Country mirrors must have HTTP URLs set.
1371+ raise MirrorHasNoHTTPURL(
1372+ "This mirror may not be set as a country mirror as it does "
1373+ "not have an HTTP URL set.")
1374+
1375+ if not self.last_probe_record:
1376+ # Only mirrors which have been probed may be set as country
1377+ # mirrors.
1378+ raise MirrorNotProbed(
1379+ "This mirror may not be set as a country mirror as it has "
1380+ "not been probed.")
1381+
1382+ # Verification done.
1383+ return True
1384+
1385+ def canTransitionToCountryMirror(self):
1386+ """See `IDistributionMirror`."""
1387+ try:
1388+ return self.verifyTransitionToCountryMirror()
1389+ except CannotTransitionToCountryMirror:
1390+ return False
1391+
1392+ def transitionToCountryMirror(self, country_dns_mirror):
1393+ """See `IDistributionMirror`."""
1394+
1395+ # country_dns_mirror has not been changed, do nothing.
1396+ if self.country_dns_mirror == country_dns_mirror:
1397+ return
1398+
1399+ # Environment sanity checks.
1400+ if country_dns_mirror:
1401+ self.verifyTransitionToCountryMirror()
1402+
1403+ self.country_dns_mirror = country_dns_mirror
1404+
1405 def getOverallFreshness(self):
1406 """See IDistributionMirror"""
1407 # XXX Guilherme Salgado 2006-08-16:
1408
1409=== modified file 'lib/lp/registry/model/person.py'
1410--- lib/lp/registry/model/person.py 2010-03-24 23:19:52 +0000
1411+++ lib/lp/registry/model/person.py 2010-04-05 20:44:34 +0000
1412@@ -124,7 +124,7 @@
1413 TeamMembershipStatus)
1414 from lp.registry.interfaces.wikiname import IWikiName, IWikiNameSet
1415 from canonical.launchpad.webapp.interfaces import (
1416- AUTH_STORE, ILaunchBag, IStoreSelector, MASTER_FLAVOR)
1417+ ILaunchBag, IStoreSelector, MASTER_FLAVOR)
1418
1419 from lp.soyuz.model.archive import Archive
1420 from lp.registry.model.codeofconduct import SignedCodeOfConduct
1421@@ -263,22 +263,7 @@
1422 return '<Person at 0x%x %s (%s)>' % (
1423 id(self), self.name, self.displayname)
1424
1425- def _sync_displayname(self, attr, value):
1426- """Update any related Account.displayname.
1427-
1428- We can't do this in a DB trigger as soon the Account table will
1429- in a separate database to the Person table.
1430- """
1431- if self.accountID is not None:
1432- auth_store = getUtility(IStoreSelector).get(
1433- AUTH_STORE, MASTER_FLAVOR)
1434- account = auth_store.get(Account, self.accountID)
1435- if account.displayname != value:
1436- account.displayname = value
1437- return value
1438-
1439- displayname = StringCol(dbName='displayname', notNull=True,
1440- storm_validator=_sync_displayname)
1441+ displayname = StringCol(dbName='displayname', notNull=True)
1442
1443 teamdescription = StringCol(dbName='teamdescription', default=None)
1444 homepage_content = StringCol(default=None)
1445
1446=== modified file 'lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt'
1447--- lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt 2009-12-10 23:32:13 +0000
1448+++ lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt 2010-04-05 20:44:34 +0000
1449@@ -40,13 +40,14 @@
1450 >>> print browser.title
1451 Mirrors of Ubuntu Linux...
1452 >>> print_mirrors_by_countries(browser.contents)
1453- Antarctica:
1454- [(u'Archive-mirror2', u'http', u'128 Kbps', u'Six hours behind')]
1455+ Antarctica: [(u'Archive-mirror2', u'http', u'128 Kbps',
1456+ u'Six hours behind')]
1457 France:
1458 [(u'Archive-404-mirror', u'http', u'512 Kbps', u'Last update unknown'),
1459- (u'Archive-mirror', u'http', u'128 Kbps', u'Last update unknown')]
1460- United Kingdom:
1461- [(u'Canonical-archive', u'http', u'100 Mbps', u'Last update unknown')]
1462+ (u'Archive-mirror', u'http', u'128 Kbps', u'Last update unknown')]
1463+ United Kingdom: [(u'Canonical-archive', u'http', u'100 Mbps',
1464+ u'Last update unknown')]
1465+
1466 >>> find_tags_by_class(browser.contents, 'distromirrorstatusSIXHOURSBEHIND')
1467 [<span class="distromirrorstatusSIXHOURSBEHIND">Six hours behind</span>]
1468 >>> find_tags_by_class(browser.contents, 'distromirrorstatusUNKNOWN')[0]
1469@@ -59,13 +60,12 @@
1470 >>> browser.url
1471 'http://launchpad.dev/ubuntu/+cdmirrors'
1472 >>> print_mirrors_by_countries(browser.contents)
1473- France:
1474- [(u'Releases-mirror', u'http', u'2 Mbps'),
1475+ France:
1476+ [(u'Releases-mirror', u'http', u'2 Mbps'),
1477 (u'Unreachable-mirror', u'http', u'512 Kbps')]
1478- Germany:
1479- [(u'Releases-mirror2', u'http', u'2 Mbps')]
1480- United Kingdom:
1481- [(u'Canonical-releases', u'http', u'100 Mbps')]
1482+ Germany: [(u'Releases-mirror2', u'http', u'2 Mbps')]
1483+ United Kingdom: [(u'Canonical-releases', u'http', u'100 Mbps')]
1484+
1485
1486 === Disabled mirrors ===
1487
1488
1489=== modified file 'lib/lp/registry/stories/person/xx-admin-person-review.txt'
1490--- lib/lp/registry/stories/person/xx-admin-person-review.txt 2010-01-30 22:39:54 +0000
1491+++ lib/lp/registry/stories/person/xx-admin-person-review.txt 2010-04-05 20:44:34 +0000
1492@@ -63,7 +63,7 @@
1493 >>> print admin_browser.title
1494 The one and only Salgado does not use Launchpad
1495 >>> print get_feedback_messages(admin_browser.contents)[0]
1496- The account "The one and only Salgado" has been suspended.
1497+ The account "Guilherme Salgado" has been suspended.
1498
1499 The admin can see the account information of a user that does not use
1500 Launchpad, and can change the account too. Note that all pages that belong
1501
1502=== modified file 'lib/lp/registry/stories/webservice/xx-distribution-mirror.txt'
1503--- lib/lp/registry/stories/webservice/xx-distribution-mirror.txt 2010-02-23 19:40:45 +0000
1504+++ lib/lp/registry/stories/webservice/xx-distribution-mirror.txt 2010-04-05 20:44:34 +0000
1505@@ -8,10 +8,12 @@
1506 >>> distro = distros['entries'][0]
1507 >>> ubuntu = webservice.get(distro['self_link']).jsonBody()
1508 >>> ubuntu_archive_mirrors = webservice.get(ubuntu['archive_mirrors_collection_link']).jsonBody()
1509- >>> canonical_archive = ubuntu_archive_mirrors['entries'][0]
1510- >>> canonical_archive_json = webservice.get(canonical_archive['self_link']).jsonBody()
1511- >>> pprint_entry(canonical_archive_json)
1512+ >>> canonical_archive = webservice.named_get(
1513+ ... ubuntu['self_link'], 'getMirrorByName',
1514+ ... name='canonical-archive').jsonBody()
1515+ >>> pprint_entry(canonical_archive)
1516 content: u'Archive'
1517+ country_dns_mirror: False
1518 country_link: u'http://.../+countries/GB'
1519 date_created: u'2006-10-16T18:31:43.434567+00:00'
1520 date_reviewed: None
1521@@ -39,6 +41,7 @@
1522 >>> canonical_releases_json = webservice.get(canonical_releases['self_link']).jsonBody()
1523 >>> pprint_entry(canonical_releases_json)
1524 content: u'CD Image'
1525+ country_dns_mirror: False
1526 country_link: u'http://.../+countries/GB'
1527 date_created: u'2006-10-16T18:31:43.434567+00:00'
1528 date_reviewed: None
1529@@ -73,12 +76,12 @@
1530 >>> karl_db = getUtility(IPersonSet).getByName('karl')
1531 >>> test_db = getUtility(IPersonSet).getByName('name12')
1532 >>> no_priv_db = getUtility(IPersonSet).getByName('no-priv')
1533- >>> karl_webservice = webservice_for_person(karl_db,
1534- ... permission=OAuthPermission.WRITE_PUBLIC)
1535- >>> test_webservice = webservice_for_person(test_db,
1536- ... permission=OAuthPermission.WRITE_PUBLIC)
1537- >>> no_priv_webservice = webservice_for_person(no_priv_db,
1538- ... permission=OAuthPermission.READ_PUBLIC)
1539+ >>> karl_webservice = webservice_for_person(
1540+ ... karl_db, permission=OAuthPermission.WRITE_PUBLIC)
1541+ >>> test_webservice = webservice_for_person(
1542+ ... test_db, permission=OAuthPermission.WRITE_PUBLIC)
1543+ >>> no_priv_webservice = webservice_for_person(
1544+ ... no_priv_db, permission=OAuthPermission.READ_PUBLIC)
1545 >>> logout()
1546
1547 Ensure that anonymous API sessions can view mirror listings; archive/releases.
1548@@ -97,7 +100,9 @@
1549
1550 One must have special permissions to access certain attributes:
1551
1552- >>> archive_404_mirror = ubuntu_archive_mirrors['entries'][1]
1553+ >>> archive_404_mirror = webservice.named_get(
1554+ ... ubuntu['self_link'], 'getMirrorByName',
1555+ ... name="archive-404-mirror").jsonBody()
1556 >>> response = no_priv_webservice.get(
1557 ... archive_404_mirror['self_link']).jsonBody()
1558 >>> pprint_entry(response)
1559@@ -128,6 +133,7 @@
1560 ... archive_404_mirror['self_link']).jsonBody()
1561 >>> pprint_entry(response)
1562 content: u'Archive'
1563+ country_dns_mirror: False
1564 country_link: u'http://.../+countries/FR'
1565 date_created: u'2006-10-16T18:31:43.438573+00:00'
1566 date_reviewed: None
1567@@ -209,6 +215,7 @@
1568 ... canonical_releases['self_link'], 'application/json', dumps(patch)).jsonBody()
1569 >>> pprint_entry(response)
1570 content: u'CD Image'
1571+ country_dns_mirror: False
1572 country_link: u'http://.../+countries/GL'
1573 date_created: u'2006-10-16T18:31:43.434567+00:00'
1574 date_reviewed: None
1575@@ -244,7 +251,9 @@
1576 "getOverallFreshness" returns the freshness of the mirror determined by the
1577 mirror prober from the mirror's last probe.
1578
1579- >>> releases_mirror2 = ubuntu_cd_mirrors['entries'][2]
1580+ >>> releases_mirror2 = webservice.named_get(
1581+ ... ubuntu['self_link'], 'getMirrorByName',
1582+ ... name='releases-mirror2').jsonBody()
1583 >>> freshness = webservice.named_get(releases_mirror2['self_link'],
1584 ... 'getOverallFreshness').jsonBody()
1585 >>> print freshness
1586
1587=== modified file 'lib/lp/registry/stories/webservice/xx-distribution.txt'
1588--- lib/lp/registry/stories/webservice/xx-distribution.txt 2010-02-23 17:36:27 +0000
1589+++ lib/lp/registry/stories/webservice/xx-distribution.txt 2010-04-05 20:44:34 +0000
1590@@ -123,6 +123,7 @@
1591 ... name='canonical-releases').jsonBody()
1592 >>> pprint_entry(canonical_releases)
1593 content: u'CD Image'
1594+ country_dns_mirror: False
1595 country_link: u'http://.../+countries/GB'
1596 date_created: u'2006-10-16T18:31:43.434567+00:00'
1597 date_reviewed: None
1598@@ -142,3 +143,76 @@
1599 speed: u'100 Mbps'
1600 status: u'Official'
1601 whiteboard: None
1602+
1603+"getCountryMirror" returns the country DNS mirror for a given country;
1604+returning None if there isn't one.
1605+
1606+ >>> # Prepare stuff.
1607+ >>> from lp.registry.interfaces.distribution import IDistributionSet
1608+ >>> from zope.component import getUtility
1609+ >>> from canonical.launchpad.testing.pages import webservice_for_person
1610+ >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
1611+ >>> from lp.registry.interfaces.person import IPersonSet
1612+ >>> from simplejson import dumps
1613+
1614+ >>> login('admin@canonical.com')
1615+ >>> ubuntu_distro = getUtility(IDistributionSet).getByName('ubuntu')
1616+ >>> showa_station = factory.makeMirror(ubuntu_distro,
1617+ ... "Showa Station", country=9,
1618+ ... http_url="http://mirror.showa.antarctica.org/ubuntu",
1619+ ... official_candidate=True)
1620+ >>> showa_station_log = factory.makeMirrorProbeRecord(showa_station)
1621+ >>> logout()
1622+
1623+ >>> login(ANONYMOUS)
1624+ >>> karl_db = getUtility(IPersonSet).getByName('karl')
1625+ >>> karl_webservice = webservice_for_person(karl_db,
1626+ ... permission=OAuthPermission.WRITE_PUBLIC)
1627+ >>> logout()
1628+
1629+ >>> # Mark new mirror as official and a country mirror.
1630+ >>> patch = {
1631+ ... u'status': 'Official',
1632+ ... u'country_dns_mirror': True
1633+ ... }
1634+
1635+ >>> antarctica_patch_target = webservice.named_get(
1636+ ... ubuntu['self_link'], 'getMirrorByName',
1637+ ... name='mirror.showa.antarctica.org-archive').jsonBody()
1638+ ... )
1639+
1640+ >>> response = karl_webservice.patch(
1641+ ... antarctica_patch_target['self_link'], 'application/json',
1642+ ... dumps(patch))
1643+
1644+ >>> antarctica = webservice.get("/+countries/AQ").jsonBody()
1645+ >>> antarctica_country_mirror_archive = webservice.named_get(
1646+ ... ubuntu['self_link'], 'getCountryMirror',
1647+ ... country=antarctica['self_link'],
1648+ ... mirror_type="Archive").jsonBody()
1649+ >>> pprint_entry(antarctica_country_mirror_archive)
1650+ content: u'Archive'
1651+ country_dns_mirror: True
1652+ country_link: u'http://.../+countries/AQ'
1653+ ...
1654+
1655+ >>> uk = webservice.get("/+countries/GB").jsonBody()
1656+ >>> uk_country_mirror_archive = webservice.named_get(
1657+ ... ubuntu['self_link'], 'getCountryMirror',
1658+ ... country=uk['self_link'],
1659+ ... mirror_type="Archive")
1660+ >>> print uk_country_mirror_archive.jsonBody()
1661+ None
1662+
1663+For "getCountryMirror", the mirror_type parameter must be "Archive" or
1664+"CD Images":
1665+
1666+ >>> uk_country_mirror_archive = webservice.named_get(
1667+ ... ubuntu['self_link'], 'getCountryMirror',
1668+ ... country=uk['self_link'],
1669+ ... mirror_type="Bogus")
1670+ >>> print uk_country_mirror_archive.jsonBody()
1671+ Traceback (most recent call last):
1672+ ...
1673+ ValueError: mirror_type: Invalid value "Bogus". Acceptable values are:
1674+ Archive, CD Image
1675
1676=== modified file 'lib/lp/registry/tests/test_distributionmirror.py'
1677--- lib/lp/registry/tests/test_distributionmirror.py 2009-10-26 18:40:04 +0000
1678+++ lib/lp/registry/tests/test_distributionmirror.py 2010-04-05 20:44:34 +0000
1679@@ -3,7 +3,6 @@
1680
1681 __metaclass__ = type
1682
1683-from StringIO import StringIO
1684 import unittest
1685
1686 import transaction
1687@@ -17,19 +16,19 @@
1688 from lp.registry.interfaces.distributionmirror import (
1689 IDistributionMirrorSet, MirrorContent, MirrorFreshness)
1690 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
1691-from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
1692 from lp.registry.interfaces.pocket import PackagePublishingPocket
1693 from lp.registry.interfaces.distribution import IDistributionSet
1694 from lp.services.mail import stub
1695+from lp.testing.factory import LaunchpadObjectFactory
1696
1697 from canonical.testing import LaunchpadFunctionalLayer
1698
1699-
1700 class TestDistributionMirror(unittest.TestCase):
1701 layer = LaunchpadFunctionalLayer
1702
1703 def setUp(self):
1704 login('test@canonical.com')
1705+ self.factory = LaunchpadObjectFactory()
1706 mirrorset = getUtility(IDistributionMirrorSet)
1707 self.cdimage_mirror = getUtility(IDistributionMirrorSet).getByName(
1708 'releases-mirror')
1709@@ -132,15 +131,6 @@
1710 self.archive_mirror.getOverallFreshness(),
1711 MirrorFreshness.TWODAYSBEHIND)
1712
1713- def _create_probe_record(self, mirror):
1714- log_file = StringIO()
1715- log_file.write("Fake probe, nothing useful here.")
1716- log_file.seek(0)
1717- library_alias = getUtility(ILibraryFileAliasSet).create(
1718- name='foo', size=len(log_file.getvalue()),
1719- file=log_file, contentType='text/plain')
1720- proberecord = mirror.newProbeRecord(library_alias)
1721-
1722 def test_disabling_mirror_and_notifying_owner(self):
1723 login('karl@canonical.com')
1724
1725@@ -148,7 +138,7 @@
1726 # If a mirror has been probed only once, the owner will always be
1727 # notified when it's disabled --it doesn't matter whether it was
1728 # previously enabled or disabled.
1729- self._create_probe_record(mirror)
1730+ self.factory.makeMirrorProbeRecord(mirror)
1731 self.failUnless(mirror.enabled)
1732 log = 'Got a 404 on http://foo/baz'
1733 mirror.disable(notify_owner=True, log=log)
1734@@ -166,7 +156,7 @@
1735
1736 # For mirrors that have been probed more than once, we'll only notify
1737 # the owner if the mirror was previously enabled.
1738- self._create_probe_record(mirror)
1739+ self.factory.makeMirrorProbeRecord(mirror)
1740 mirror.enabled = True
1741 mirror.disable(notify_owner=True, log=log)
1742 # A notification was sent to the owner and other to the mirror admins.
1743
1744=== modified file 'lib/lp/registry/tests/test_personset.py'
1745--- lib/lp/registry/tests/test_personset.py 2010-03-11 20:54:36 +0000
1746+++ lib/lp/registry/tests/test_personset.py 2010-04-05 20:44:34 +0000
1747@@ -99,23 +99,19 @@
1748 # Person in question.
1749
1750 # Create a testing `Account` and a testing `Person` directly,
1751- # linked. However the `Account` email is not linked to the
1752- # `Person`.
1753+ # linked.
1754 testing_account = self.factory.makeAccount(
1755 self.displayname, email=self.email_address)
1756 testing_person = removeSecurityProxy(
1757 testing_account).createPerson(self.rationale)
1758- self.assertIs(None, testing_account.preferredemail.person)
1759- self.assertIs(None, testing_person.preferredemail)
1760+ self.assertEqual(
1761+ testing_person, testing_account.preferredemail.person)
1762
1763+ # Since there's an existing Person for the given email address,
1764+ # IPersonSet.ensurePerson() will just return it.
1765 ensured_person = self.person_set.ensurePerson(
1766 self.email_address, self.displayname, self.rationale)
1767-
1768- # The existing Person was retrieved and the Account
1769- # 'preferredemail' is also bound to the existing Person.
1770- self.assertEquals(testing_person.id, ensured_person.id)
1771- self.assertEquals(testing_account.preferredemail.id,
1772- ensured_person.preferredemail.id)
1773+ self.assertEqual(testing_person, ensured_person)
1774
1775
1776 class TestPersonSetMerge(TestCaseWithFactory):
1777
1778=== modified file 'lib/lp/testing/factory.py'
1779--- lib/lp/testing/factory.py 2010-03-26 21:03:30 +0000
1780+++ lib/lp/testing/factory.py 2010-04-05 20:44:34 +0000
1781@@ -1992,8 +1992,22 @@
1782 team_list = self.makeMailingList(team, owner)
1783 return team, team_list
1784
1785+ def makeMirrorProbeRecord(self, mirror):
1786+ """Create a probe record for a mirror of a distribution."""
1787+ log_file = StringIO()
1788+ log_file.write("Fake probe, nothing useful here.")
1789+ log_file.seek(0)
1790+
1791+ library_alias = getUtility(ILibraryFileAliasSet).create(
1792+ name='foo', size=len(log_file.getvalue()),
1793+ file=log_file, contentType='text/plain')
1794+
1795+ proberecord = mirror.newProbeRecord(library_alias)
1796+ return proberecord
1797+
1798 def makeMirror(self, distribution, displayname, country=None,
1799- http_url=None, ftp_url=None, rsync_url=None):
1800+ http_url=None, ftp_url=None, rsync_url=None,
1801+ official_candidate=False):
1802 """Create a mirror for the distribution."""
1803 # If no URL is specified create an HTTP URL.
1804 if http_url is None and ftp_url is None and rsync_url is None:
1805@@ -2012,7 +2026,7 @@
1806 http_base_url=http_url,
1807 ftp_base_url=ftp_url,
1808 rsync_base_url=rsync_url,
1809- official_candidate=False)
1810+ official_candidate=official_candidate)
1811 return mirror
1812
1813 def makeUniqueRFC822MsgId(self):
1814
1815=== modified file 'lib/lp/testopenid/browser/server.py'
1816--- lib/lp/testopenid/browser/server.py 2010-02-24 12:52:08 +0000
1817+++ lib/lp/testopenid/browser/server.py 2010-04-05 20:44:34 +0000
1818@@ -6,7 +6,7 @@
1819 __all__ = [
1820 'PersistentIdentityView',
1821 'TestOpenIDApplicationNavigation',
1822- 'TestOpenIDIndexView'
1823+ 'TestOpenIDIndexView',
1824 'TestOpenIDLoginView',
1825 'TestOpenIDRootUrlData',
1826 'TestOpenIDView',
1827@@ -21,6 +21,7 @@
1828 from zope.security.proxy import isinstance as zisinstance
1829 from zope.session.interfaces import ISession
1830
1831+from openid import oidutil
1832 from openid.server.server import CheckIDRequest, Server
1833 from openid.store.memstore import MemoryStore
1834
1835@@ -47,6 +48,10 @@
1836 openid_store = MemoryStore()
1837
1838
1839+# Shut up noisy OpenID library
1840+oidutil.log = lambda message, level=0: None
1841+
1842+
1843 class TestOpenIDRootUrlData:
1844 """`ICanonicalUrlData` for the test OpenID provider."""
1845
1846
1847=== modified file 'utilities/sourcedeps.conf'
1848--- utilities/sourcedeps.conf 2010-03-26 17:53:41 +0000
1849+++ utilities/sourcedeps.conf 2010-04-05 20:44:34 +0000
1850@@ -15,5 +15,4 @@
1851 subunit lp:~launchpad-pqm/subunit/trunk;revno=61
1852 subvertpy lp:~launchpad-pqm/subvertpy/trunk;revno=2040
1853 testresources lp:~launchpad-pqm/testresources/dev;revno=16
1854-canonical-identity-provider lp:~launchpad-pqm/canonical-identity-provider/trunk;revno=8903 optional
1855 shipit lp:~launchpad-pqm/shipit/trunk;revno=8903 optional

Subscribers

People subscribed via source and target branches

to status/vote changes: