Merge launchpad:master into launchpad:db-devel

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 7cc2b100ff6cee4a1af522ffb6bd19200b0bc011
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: launchpad:master
Merge into: launchpad:db-devel
Diff against target: 1786 lines (+576/-268)
41 files modified
lib/lp/archivepublisher/archivegpgsigningkey.py (+55/-23)
lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py (+105/-3)
lib/lp/blueprints/browser/sprint.py (+3/-2)
lib/lp/blueprints/browser/tests/test_views.py (+2/-1)
lib/lp/blueprints/model/specification.py (+14/-13)
lib/lp/blueprints/model/sprint.py (+96/-70)
lib/lp/blueprints/model/sprintspecification.py (+39/-23)
lib/lp/blueprints/vocabularies/sprint.py (+5/-9)
lib/lp/code/mail/tests/test_codehandler.py (+2/-0)
lib/lp/code/mail/tests/test_codereviewcomment.py (+1/-1)
lib/lp/code/model/branchcollection.py (+2/-2)
lib/lp/code/model/branchmergeproposal.py (+28/-15)
lib/lp/code/model/codereviewcomment.py (+30/-14)
lib/lp/code/model/codereviewvote.py (+36/-20)
lib/lp/code/model/gitcollection.py (+2/-2)
lib/lp/code/model/tests/test_branch.py (+4/-4)
lib/lp/code/model/tests/test_gitrepository.py (+4/-4)
lib/lp/code/stories/webservice/xx-branchmergeproposal.txt (+2/-2)
lib/lp/codehosting/puller/tests/test_scheduler.py (+1/-1)
lib/lp/registry/model/projectgroup.py (+13/-10)
lib/lp/services/database/policy.py (+1/-1)
lib/lp/services/gpg/handler.py (+3/-7)
lib/lp/services/gpg/interfaces.py (+13/-2)
lib/lp/services/librarianserver/tests/test_storage_db.py (+1/-1)
lib/lp/services/mail/helpers.py (+11/-3)
lib/lp/services/signing/tests/helpers.py (+2/-2)
lib/lp/services/worlddata/vocabularies.py (+4/-1)
lib/lp/soyuz/adapters/tests/test_archivedependencies.py (+1/-1)
lib/lp/soyuz/configure.zcml (+0/-1)
lib/lp/soyuz/interfaces/archive.py (+2/-0)
lib/lp/soyuz/model/archive.py (+7/-6)
lib/lp/soyuz/scripts/ppakeygenerator.py (+3/-3)
lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py (+5/-5)
lib/lp/soyuz/stories/soyuz/xx-person-packages.txt (+1/-1)
lib/lp/soyuz/tests/test_archive.py (+1/-1)
lib/lp/testing/factory.py (+4/-4)
lib/lp/translations/pottery/tests/test_detect_intltool.py (+14/-0)
scripts/rosetta/pottery-generate-intltool.py (+56/-0)
utilities/launchpad-database-setup (+0/-7)
utilities/sourcedeps.cache (+2/-2)
utilities/sourcedeps.conf (+1/-1)
Reviewer Review Type Date Requested Status
Colin Watson Approve
Review via email: mp+391043@code.launchpad.net

Commit message

Manually merge from master to fix tests with PostgreSQL 10 base image

Description of the change

Now that buildbot is on LXD with fresh base images, db-devel can't pass tests until https://code.launchpad.net/~twom/launchpad/+git/launchpad/+merge/391040 is merged.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/archivepublisher/archivegpgsigningkey.py b/lib/lp/archivepublisher/archivegpgsigningkey.py
2index a342332..20a4d79 100644
3--- a/lib/lp/archivepublisher/archivegpgsigningkey.py
4+++ b/lib/lp/archivepublisher/archivegpgsigningkey.py
5@@ -37,17 +37,24 @@ from lp.archivepublisher.run_parts import (
6 from lp.registry.interfaces.gpg import IGPGKeySet
7 from lp.services.config import config
8 from lp.services.features import getFeatureFlag
9-from lp.services.gpg.interfaces import IGPGHandler
10+from lp.services.gpg.interfaces import (
11+ IGPGHandler,
12+ IPymeKey,
13+ )
14 from lp.services.osutils import remove_if_exists
15 from lp.services.propertycache import (
16 cachedproperty,
17 get_property_cache,
18 )
19 from lp.services.signing.enums import (
20+ OpenPGPKeyAlgorithm,
21 SigningKeyType,
22 SigningMode,
23 )
24-from lp.services.signing.interfaces.signingkey import ISigningKeySet
25+from lp.services.signing.interfaces.signingkey import (
26+ ISigningKey,
27+ ISigningKeySet,
28+ )
29
30
31 @implementer(ISignableArchive)
32@@ -72,7 +79,7 @@ class SignableArchive:
33 def can_sign(self):
34 """See `ISignableArchive`."""
35 return (
36- self.archive.signing_key is not None or
37+ self.archive.signing_key_fingerprint is not None or
38 self._run_parts_dir is not None)
39
40 @cachedproperty
41@@ -237,9 +244,9 @@ class ArchiveGPGSigningKey(SignableArchive):
42 with open(export_path, 'wb') as export_file:
43 export_file.write(key.export())
44
45- def generateSigningKey(self, log=None):
46+ def generateSigningKey(self, log=None, async_keyserver=False):
47 """See `IArchiveGPGSigningKey`."""
48- assert self.archive.signing_key is None, (
49+ assert self.archive.signing_key_fingerprint is None, (
50 "Cannot override signing_keys.")
51
52 # Always generate signing keys for the default PPA, even if it
53@@ -257,13 +264,26 @@ class ArchiveGPGSigningKey(SignableArchive):
54
55 key_displayname = (
56 "Launchpad PPA for %s" % self.archive.owner.displayname)
57- secret_key = getUtility(IGPGHandler).generateKey(
58- key_displayname, logger=log)
59- self._setupSigningKey(secret_key)
60+ if getFeatureFlag(PUBLISHER_GPG_USES_SIGNING_SERVICE):
61+ try:
62+ signing_key = getUtility(ISigningKeySet).generate(
63+ SigningKeyType.OPENPGP, key_displayname,
64+ openpgp_key_algorithm=OpenPGPKeyAlgorithm.RSA, length=4096)
65+ except Exception as e:
66+ if log is not None:
67+ log.exception(
68+ "Error generating signing key for %s: %s %s" %
69+ (self.archive.reference, e.__class__.__name__, e))
70+ raise
71+ else:
72+ signing_key = getUtility(IGPGHandler).generateKey(
73+ key_displayname, logger=log)
74+ return self._setupSigningKey(
75+ signing_key, async_keyserver=async_keyserver)
76
77 def setSigningKey(self, key_path, async_keyserver=False):
78 """See `IArchiveGPGSigningKey`."""
79- assert self.archive.signing_key is None, (
80+ assert self.archive.signing_key_fingerprint is None, (
81 "Cannot override signing_keys.")
82 assert os.path.exists(key_path), (
83 "%s does not exist" % key_path)
84@@ -274,34 +294,46 @@ class ArchiveGPGSigningKey(SignableArchive):
85 return self._setupSigningKey(
86 secret_key, async_keyserver=async_keyserver)
87
88- def _uploadPublicSigningKey(self, secret_key):
89+ def _uploadPublicSigningKey(self, signing_key):
90 """Upload the public half of a signing key to the keyserver."""
91 # The handler's security proxying doesn't protect anything useful
92 # here, and when we're running in a thread we don't have an
93 # interaction.
94 gpghandler = removeSecurityProxy(getUtility(IGPGHandler))
95- pub_key = gpghandler.retrieveKey(secret_key.fingerprint)
96- gpghandler.uploadPublicKey(pub_key.fingerprint)
97- return pub_key
98+ if IPymeKey.providedBy(signing_key):
99+ pub_key = gpghandler.retrieveKey(signing_key.fingerprint)
100+ gpghandler.uploadPublicKey(pub_key.fingerprint)
101+ return pub_key
102+ else:
103+ assert ISigningKey.providedBy(signing_key)
104+ gpghandler.submitKey(removeSecurityProxy(signing_key).public_key)
105+ return signing_key
106
107 def _storeSigningKey(self, pub_key):
108 """Store signing key reference in the database."""
109 key_owner = getUtility(ILaunchpadCelebrities).ppa_key_guard
110- key, _ = getUtility(IGPGKeySet).activate(
111- key_owner, pub_key, pub_key.can_encrypt)
112- self.archive.signing_key_owner = key.owner
113+ if IPymeKey.providedBy(pub_key):
114+ key, _ = getUtility(IGPGKeySet).activate(
115+ key_owner, pub_key, pub_key.can_encrypt)
116+ else:
117+ assert ISigningKey.providedBy(pub_key)
118+ key = pub_key
119+ self.archive.signing_key_owner = key_owner
120 self.archive.signing_key_fingerprint = key.fingerprint
121 del get_property_cache(self.archive).signing_key
122
123- def _setupSigningKey(self, secret_key, async_keyserver=False):
124+ def _setupSigningKey(self, signing_key, async_keyserver=False):
125 """Mandatory setup for signing keys.
126
127- * Export the secret key into the protected disk location.
128+ * Export the secret key into the protected disk location (for
129+ locally-generated keys).
130 * Upload public key to the keyserver.
131- * Store the public GPGKey reference in the database and update
132- the context archive.signing_key.
133+ * Store the public GPGKey reference in the database (for
134+ locally-generated keys) and update the context
135+ archive.signing_key.
136 """
137- self.exportSecretKey(secret_key)
138+ if IPymeKey.providedBy(signing_key):
139+ self.exportSecretKey(signing_key)
140 if async_keyserver:
141 # If we have an asynchronous keyserver running in the current
142 # thread using Twisted, then we need some contortions to ensure
143@@ -310,10 +342,10 @@ class ArchiveGPGSigningKey(SignableArchive):
144 # Since that thread won't have a Zope interaction, we need to
145 # unwrap the security proxy for it.
146 d = deferToThread(
147- self._uploadPublicSigningKey, removeSecurityProxy(secret_key))
148+ self._uploadPublicSigningKey, removeSecurityProxy(signing_key))
149 d.addCallback(ProxyFactory)
150 d.addCallback(self._storeSigningKey)
151 return d
152 else:
153- pub_key = self._uploadPublicSigningKey(secret_key)
154+ pub_key = self._uploadPublicSigningKey(signing_key)
155 self._storeSigningKey(pub_key)
156diff --git a/lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py b/lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py
157index 87359e8..67366bd 100644
158--- a/lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py
159+++ b/lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py
160@@ -14,8 +14,15 @@ from testtools.matchers import (
161 FileContains,
162 StartsWith,
163 )
164-from testtools.twistedsupport import AsynchronousDeferredRunTest
165-from twisted.internet import defer
166+from testtools.twistedsupport import (
167+ AsynchronousDeferredRunTest,
168+ AsynchronousDeferredRunTestForBrokenTwisted,
169+ )
170+import treq
171+from twisted.internet import (
172+ defer,
173+ reactor,
174+ )
175 from zope.component import getUtility
176
177 from lp.archivepublisher.config import getPubConfig
178@@ -26,18 +33,27 @@ from lp.archivepublisher.interfaces.archivegpgsigningkey import (
179 )
180 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
181 from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
182+from lp.registry.interfaces.gpg import IGPGKeySet
183 from lp.services.compat import mock
184 from lp.services.features.testing import FeatureFixture
185+from lp.services.gpg.interfaces import IGPGHandler
186+from lp.services.gpg.tests.test_gpghandler import FakeGenerateKey
187 from lp.services.log.logger import BufferLogger
188 from lp.services.osutils import write_file
189 from lp.services.signing.enums import (
190 SigningKeyType,
191 SigningMode,
192 )
193+from lp.services.signing.interfaces.signingkey import ISigningKeySet
194 from lp.services.signing.tests.helpers import SigningServiceClientFixture
195+from lp.services.twistedsupport.testing import TReqFixture
196+from lp.services.twistedsupport.treq import check_status
197 from lp.soyuz.enums import ArchivePurpose
198 from lp.testing import TestCaseWithFactory
199-from lp.testing.gpgkeys import gpgkeysdir
200+from lp.testing.gpgkeys import (
201+ gpgkeysdir,
202+ test_pubkey_from_email,
203+ )
204 from lp.testing.keyserver import InProcessKeyServerFixture
205 from lp.testing.layers import ZopelessDatabaseLayer
206
207@@ -271,3 +287,89 @@ class TestSignableArchiveWithRunParts(RunPartsMixin, TestCaseWithFactory):
208 FileContains(
209 "detached signature of %s (%s, %s/%s)\n" %
210 (filename, self.archive_root, self.distro.name, self.suite)))
211+
212+
213+class TestArchiveGPGSigningKey(TestCaseWithFactory):
214+
215+ layer = ZopelessDatabaseLayer
216+ # treq.content doesn't close the connection before yielding control back
217+ # to the test, so we need to spin the reactor at the end to finish
218+ # things off.
219+ run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory(
220+ timeout=10000)
221+
222+ @defer.inlineCallbacks
223+ def setUp(self):
224+ super(TestArchiveGPGSigningKey, self).setUp()
225+ self.temp_dir = self.makeTemporaryDirectory()
226+ self.pushConfig("personalpackagearchive", root=self.temp_dir)
227+ self.keyserver = self.useFixture(InProcessKeyServerFixture())
228+ yield self.keyserver.start()
229+
230+ @defer.inlineCallbacks
231+ def test_generateSigningKey_local(self):
232+ # Generating a signing key locally using GPGHandler stores it in the
233+ # database and pushes it to the keyserver.
234+ self.useFixture(FakeGenerateKey("ppa-sample@canonical.com.sec"))
235+ logger = BufferLogger()
236+ # Use a display name that matches the pregenerated sample key.
237+ owner = self.factory.makePerson(
238+ displayname="Celso \xe1\xe9\xed\xf3\xfa Providelo")
239+ archive = self.factory.makeArchive(owner=owner)
240+ yield IArchiveGPGSigningKey(archive).generateSigningKey(
241+ log=logger, async_keyserver=True)
242+ # The key is stored in the database.
243+ self.assertIsNotNone(archive.signing_key_owner)
244+ self.assertIsNotNone(archive.signing_key_fingerprint)
245+ # The key is stored as a GPGKey, not a SigningKey.
246+ self.assertIsNotNone(
247+ getUtility(IGPGKeySet).getByFingerprint(
248+ archive.signing_key_fingerprint))
249+ self.assertIsNone(
250+ getUtility(ISigningKeySet).get(
251+ SigningKeyType.OPENPGP, archive.signing_key_fingerprint))
252+ # The key is uploaded to the keyserver.
253+ client = self.useFixture(TReqFixture(reactor)).client
254+ response = yield client.get(
255+ getUtility(IGPGHandler).getURLForKeyInServer(
256+ archive.signing_key_fingerprint, "get"))
257+ yield check_status(response)
258+ content = yield treq.content(response)
259+ self.assertIn(b"-----BEGIN PGP PUBLIC KEY BLOCK-----\n", content)
260+
261+ @defer.inlineCallbacks
262+ def test_generateSigningKey_signing_service(self):
263+ # Generating a signing key on the signing service stores it in the
264+ # database and pushes it to the keyserver.
265+ self.useFixture(
266+ FeatureFixture({PUBLISHER_GPG_USES_SIGNING_SERVICE: "on"}))
267+ signing_service_client = self.useFixture(
268+ SigningServiceClientFixture(self.factory))
269+ signing_service_client.generate.side_effect = None
270+ test_key = test_pubkey_from_email("ftpmaster@canonical.com")
271+ signing_service_client.generate.return_value = {
272+ "fingerprint": "33C0A61893A5DC5EB325B29E415A12CAC2F30234",
273+ "public-key": test_key,
274+ }
275+ logger = BufferLogger()
276+ archive = self.factory.makeArchive()
277+ yield IArchiveGPGSigningKey(archive).generateSigningKey(
278+ log=logger, async_keyserver=True)
279+ # The key is stored in the database.
280+ self.assertIsNotNone(archive.signing_key_owner)
281+ self.assertIsNotNone(archive.signing_key_fingerprint)
282+ # The key is stored as a SigningKey, not a GPGKey.
283+ self.assertIsNone(
284+ getUtility(IGPGKeySet).getByFingerprint(
285+ archive.signing_key_fingerprint))
286+ signing_key = getUtility(ISigningKeySet).get(
287+ SigningKeyType.OPENPGP, archive.signing_key_fingerprint)
288+ self.assertEqual(test_key, signing_key.public_key)
289+ # The key is uploaded to the keyserver.
290+ client = self.useFixture(TReqFixture(reactor)).client
291+ response = yield client.get(
292+ getUtility(IGPGHandler).getURLForKeyInServer(
293+ archive.signing_key_fingerprint, "get"))
294+ yield check_status(response)
295+ content = yield treq.content(response)
296+ self.assertIn(test_key, content)
297diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py
298index aae704a..da94d43 100644
299--- a/lib/lp/blueprints/browser/sprint.py
300+++ b/lib/lp/blueprints/browser/sprint.py
301@@ -1,4 +1,4 @@
302-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
303+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
304 # GNU Affero General Public License version 3 (see the file LICENSE).
305
306 """Sprint views."""
307@@ -27,8 +27,8 @@ from collections import defaultdict
308 import csv
309
310 from lazr.restful.utils import smartquote
311-import six
312 import pytz
313+import six
314 from zope.component import getUtility
315 from zope.formlib.widget import CustomWidgetFactory
316 from zope.formlib.widgets import TextAreaWidget
317@@ -462,6 +462,7 @@ class SprintTopicSetView(HasSpecificationsView, LaunchpadView):
318 # only a single item was selected, but we want to deal with a
319 # list for the general case, so convert it to a list
320 selected_specs = [selected_specs]
321+ selected_specs = [int(speclink) for speclink in selected_specs]
322
323 if action == 'Accepted':
324 action_fn = self.context.acceptSpecificationLinks
325diff --git a/lib/lp/blueprints/browser/tests/test_views.py b/lib/lp/blueprints/browser/tests/test_views.py
326index 5b68b37..cbbe2e5 100644
327--- a/lib/lp/blueprints/browser/tests/test_views.py
328+++ b/lib/lp/blueprints/browser/tests/test_views.py
329@@ -110,7 +110,8 @@ def test_suite():
330 for filename in filenames:
331 path = filename
332 one_test = LayeredDocFileSuite(
333- path, setUp=setUp, tearDown=tearDown,
334+ path,
335+ setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
336 layer=DatabaseFunctionalLayer,
337 stdout_logging_level=logging.WARNING)
338 suite.addTest(one_test)
339diff --git a/lib/lp/blueprints/model/specification.py b/lib/lp/blueprints/model/specification.py
340index 40cf6ab..4b37d10 100644
341--- a/lib/lp/blueprints/model/specification.py
342+++ b/lib/lp/blueprints/model/specification.py
343@@ -23,14 +23,15 @@ from sqlobject import (
344 SQLRelatedJoin,
345 StringCol,
346 )
347-from storm.expr import (
348+from storm.locals import (
349 Count,
350 Desc,
351 Join,
352 Or,
353+ ReferenceSet,
354 SQL,
355+ Store,
356 )
357-from storm.store import Store
358 from zope.component import getUtility
359 from zope.event import notify
360 from zope.interface import implementer
361@@ -237,11 +238,13 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
362 joinColumn='specification', otherColumn='person',
363 intermediateTable='SpecificationSubscription',
364 orderBy=['display_name', 'name'])
365- sprint_links = SQLMultipleJoin('SprintSpecification', orderBy='id',
366- joinColumn='specification')
367- sprints = SQLRelatedJoin('Sprint', orderBy='name',
368- joinColumn='specification', otherColumn='sprint',
369- intermediateTable='SprintSpecification')
370+ sprint_links = ReferenceSet(
371+ '<primary key>', 'SprintSpecification.specification_id',
372+ order_by='SprintSpecification.id')
373+ sprints = ReferenceSet(
374+ '<primary key>', 'SprintSpecification.specification_id',
375+ 'SprintSpecification.sprint_id', 'Sprint.id',
376+ order_by='Sprint.name')
377 spec_dependency_links = SQLMultipleJoin('SpecificationDependency',
378 joinColumn='specification', orderBy='id')
379
380@@ -827,13 +830,11 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
381
382 def unlinkSprint(self, sprint):
383 """See ISpecification."""
384- from lp.blueprints.model.sprintspecification import (
385- SprintSpecification)
386 for sprint_link in self.sprint_links:
387 # sprints have unique names
388 if sprint_link.sprint.name == sprint.name:
389- SprintSpecification.delete(sprint_link.id)
390- return sprint_link
391+ sprint_link.destroySelf()
392+ return
393
394 # dependencies
395 def createDependency(self, specification):
396@@ -1060,8 +1061,8 @@ class SpecificationSet(HasSpecificationsMixin):
397 def coming_sprints(self):
398 """See ISpecificationSet."""
399 from lp.blueprints.model.sprint import Sprint
400- return Sprint.select("time_ends > 'NOW'", orderBy='time_starts',
401- limit=5)
402+ rows = IStore(Sprint).find(Sprint, Sprint.time_ends > UTC_NOW)
403+ return rows.order_by(Sprint.time_starts).config(limit=5)
404
405 def new(self, name, title, specurl, summary, definition_status,
406 owner, target, approver=None, assignee=None, drafter=None,
407diff --git a/lib/lp/blueprints/model/sprint.py b/lib/lp/blueprints/model/sprint.py
408index 2446437..ed6748a 100644
409--- a/lib/lp/blueprints/model/sprint.py
410+++ b/lib/lp/blueprints/model/sprint.py
411@@ -1,4 +1,4 @@
412-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
413+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
414 # GNU Affero General Public License version 3 (see the file LICENSE).
415
416 __metaclass__ = type
417@@ -8,17 +8,17 @@ __all__ = [
418 'HasSprintsMixin',
419 ]
420
421-
422-from sqlobject import (
423- BoolCol,
424- ForeignKey,
425- StringCol,
426- )
427+import pytz
428 from storm.locals import (
429+ Bool,
430+ DateTime,
431 Desc,
432+ Int,
433 Join,
434 Or,
435+ Reference,
436 Store,
437+ Unicode,
438 )
439 from zope.component import getUtility
440 from zope.interface import implementer
441@@ -38,7 +38,10 @@ from lp.blueprints.interfaces.sprint import (
442 ISprint,
443 ISprintSet,
444 )
445-from lp.blueprints.model.specification import HasSpecificationsMixin
446+from lp.blueprints.model.specification import (
447+ HasSpecificationsMixin,
448+ Specification,
449+ )
450 from lp.blueprints.model.specificationsearch import (
451 get_specification_active_product_filter,
452 get_specification_filters,
453@@ -51,46 +54,66 @@ from lp.registry.interfaces.person import (
454 validate_public_person,
455 )
456 from lp.registry.model.hasdrivers import HasDriversMixin
457-from lp.services.database.constants import DEFAULT
458-from lp.services.database.datetimecol import UtcDateTimeCol
459-from lp.services.database.sqlbase import (
460- flush_database_updates,
461- quote,
462- SQLBase,
463+from lp.services.database.constants import (
464+ DEFAULT,
465+ UTC_NOW,
466 )
467+from lp.services.database.interfaces import IStore
468+from lp.services.database.sqlbase import flush_database_updates
469+from lp.services.database.stormbase import StormBase
470 from lp.services.propertycache import cachedproperty
471
472
473 @implementer(ISprint, IHasLogo, IHasMugshot, IHasIcon)
474-class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):
475+class Sprint(StormBase, HasDriversMixin, HasSpecificationsMixin):
476 """See `ISprint`."""
477
478- _defaultOrder = ['name']
479+ __storm_table__ = 'Sprint'
480+ __storm_order__ = ['name']
481
482 # db field names
483- owner = ForeignKey(
484- dbName='owner', foreignKey='Person',
485- storm_validator=validate_public_person, notNull=True)
486- name = StringCol(notNull=True, alternateID=True)
487- title = StringCol(notNull=True)
488- summary = StringCol(notNull=True)
489- driver = ForeignKey(
490- dbName='driver', foreignKey='Person',
491- storm_validator=validate_public_person)
492- home_page = StringCol(notNull=False, default=None)
493- homepage_content = StringCol(default=None)
494- icon = ForeignKey(
495- dbName='icon', foreignKey='LibraryFileAlias', default=None)
496- logo = ForeignKey(
497- dbName='logo', foreignKey='LibraryFileAlias', default=None)
498- mugshot = ForeignKey(
499- dbName='mugshot', foreignKey='LibraryFileAlias', default=None)
500- address = StringCol(notNull=False, default=None)
501- datecreated = UtcDateTimeCol(notNull=True, default=DEFAULT)
502- time_zone = StringCol(notNull=True)
503- time_starts = UtcDateTimeCol(notNull=True)
504- time_ends = UtcDateTimeCol(notNull=True)
505- is_physical = BoolCol(notNull=True, default=True)
506+ id = Int(primary=True)
507+ owner_id = Int(
508+ name='owner', validator=validate_public_person, allow_none=False)
509+ owner = Reference(owner_id, 'Person.id')
510+ name = Unicode(allow_none=False)
511+ title = Unicode(allow_none=False)
512+ summary = Unicode(allow_none=False)
513+ driver_id = Int(name='driver', validator=validate_public_person)
514+ driver = Reference(driver_id, 'Person.id')
515+ home_page = Unicode(allow_none=True, default=None)
516+ homepage_content = Unicode(default=None)
517+ icon_id = Int(name='icon', default=None)
518+ icon = Reference(icon_id, 'LibraryFileAlias.id')
519+ logo_id = Int(name='logo', default=None)
520+ logo = Reference(logo_id, 'LibraryFileAlias.id')
521+ mugshot_id = Int(name='mugshot', default=None)
522+ mugshot = Reference(mugshot_id, 'LibraryFileAlias.id')
523+ address = Unicode(allow_none=True, default=None)
524+ datecreated = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
525+ time_zone = Unicode(allow_none=False)
526+ time_starts = DateTime(tzinfo=pytz.UTC, allow_none=False)
527+ time_ends = DateTime(tzinfo=pytz.UTC, allow_none=False)
528+ is_physical = Bool(allow_none=False, default=True)
529+
530+ def __init__(self, owner, name, title, time_zone, time_starts, time_ends,
531+ summary, address=None, driver=None, home_page=None,
532+ mugshot=None, logo=None, icon=None, is_physical=True):
533+ super(Sprint, self).__init__()
534+ self.owner = owner
535+ self.name = name
536+ self.title = title
537+ self.time_zone = time_zone
538+ self.time_starts = time_starts
539+ self.time_ends = time_ends
540+ self.summary = summary
541+ self.address = address
542+ self.driver = driver
543+ self.home_page = home_page
544+ self.mugshot = mugshot
545+ self.logo = logo
546+ self.icon = icon
547+ self.is_physical = is_physical
548
549 # attributes
550
551@@ -128,7 +151,7 @@ class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):
552 tables.append(Join(
553 SprintSpecification,
554 SprintSpecification.specification == Specification.id))
555- query.append(SprintSpecification.sprintID == self.id)
556+ query.append(SprintSpecification.sprint == self)
557
558 if not filter:
559 # filter could be None or [] then we decide the default
560@@ -209,7 +232,7 @@ class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):
561 context. Here we are a sprint that could cover many products and/or
562 distros.
563 """
564- speclink = SprintSpecification.get(speclink_id)
565+ speclink = Store.of(self).get(SprintSpecification, speclink_id)
566 assert (speclink.sprint.id == self.id)
567 return speclink
568
569@@ -303,15 +326,16 @@ class SprintSet:
570
571 def __getitem__(self, name):
572 """See `ISprintSet`."""
573- return Sprint.selectOneBy(name=name)
574+ return IStore(Sprint).find(Sprint, name=name).one()
575
576 def __iter__(self):
577 """See `ISprintSet`."""
578- return iter(Sprint.select("time_ends > 'NOW'", orderBy='time_starts'))
579+ return iter(IStore(Sprint).find(
580+ Sprint, Sprint.time_ends > UTC_NOW).order_by(Sprint.time_starts))
581
582 @property
583 def all(self):
584- return Sprint.select(orderBy='-time_starts')
585+ return IStore(Sprint).find(Sprint).order_by(Sprint.time_starts)
586
587 def new(self, owner, name, title, time_zone, time_starts, time_ends,
588 summary, address=None, driver=None, home_page=None,
589@@ -329,48 +353,50 @@ class HasSprintsMixin:
590 implementing IHasSprints.
591 """
592
593- def _getBaseQueryAndClauseTablesForQueryingSprints(self):
594- """Return the base SQL query and the clauseTables to be used when
595- querying sprints related to this object.
596+ def _getBaseClausesForQueryingSprints(self):
597+ """Return the base Storm clauses to be used when querying sprints
598+ related to this object.
599
600 Subclasses must overwrite this method if it doesn't suit them.
601 """
602- query = """
603- Specification.%s = %s
604- AND Specification.id = SprintSpecification.specification
605- AND SprintSpecification.sprint = Sprint.id
606- AND SprintSpecification.status = %s
607- """ % (self._table, self.id,
608- quote(SprintSpecificationStatus.ACCEPTED))
609- return query, ['Specification', 'SprintSpecification']
610+ try:
611+ table = getattr(self, "__storm_table__")
612+ except AttributeError:
613+ # XXX cjwatson 2020-09-10: Remove this once all inheritors have
614+ # been converted from SQLObject to Storm.
615+ table = getattr(self, "_table")
616+ return [
617+ getattr(Specification, table.lower()) == self,
618+ Specification.id == SprintSpecification.specification_id,
619+ SprintSpecification.sprint == Sprint.id,
620+ SprintSpecification.status == SprintSpecificationStatus.ACCEPTED,
621+ ]
622
623 def getSprints(self):
624- query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()
625- return Sprint.select(
626- query, clauseTables=tables, orderBy='-time_starts', distinct=True)
627+ clauses = self._getBaseClausesForQueryingSprints()
628+ return IStore(Sprint).find(Sprint, *clauses).order_by(
629+ Desc(Sprint.time_starts)).config(distinct=True)
630
631 @cachedproperty
632 def sprints(self):
633 """See IHasSprints."""
634 return list(self.getSprints())
635
636- def getComingSprings(self):
637- query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()
638- query += " AND Sprint.time_ends > 'NOW'"
639- return Sprint.select(
640- query, clauseTables=tables, orderBy='time_starts',
641- distinct=True, limit=5)
642+ def getComingSprints(self):
643+ clauses = self._getBaseClausesForQueryingSprints()
644+ clauses.append(Sprint.time_ends > UTC_NOW)
645+ return IStore(Sprint).find(Sprint, *clauses).order_by(
646+ Sprint.time_starts).config(distinct=True, limit=5)
647
648 @cachedproperty
649 def coming_sprints(self):
650 """See IHasSprints."""
651- return list(self.getComingSprings())
652+ return list(self.getComingSprints())
653
654 @property
655 def past_sprints(self):
656 """See IHasSprints."""
657- query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()
658- query += " AND Sprint.time_ends <= 'NOW'"
659- return Sprint.select(
660- query, clauseTables=tables, orderBy='-time_starts',
661- distinct=True)
662+ clauses = self._getBaseClausesForQueryingSprints()
663+ clauses.append(Sprint.time_ends <= UTC_NOW)
664+ return IStore(Sprint).find(Sprint, *clauses).order_by(
665+ Desc(Sprint.time_starts)).config(distinct=True)
666diff --git a/lib/lp/blueprints/model/sprintspecification.py b/lib/lp/blueprints/model/sprintspecification.py
667index 46e691a..eed7649 100644
668--- a/lib/lp/blueprints/model/sprintspecification.py
669+++ b/lib/lp/blueprints/model/sprintspecification.py
670@@ -1,13 +1,17 @@
671-# Copyright 2009 Canonical Ltd. This software is licensed under the
672+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
673 # GNU Affero General Public License version 3 (see the file LICENSE).
674
675 __metaclass__ = type
676
677 __all__ = ['SprintSpecification']
678
679-from sqlobject import (
680- ForeignKey,
681- StringCol,
682+import pytz
683+from storm.locals import (
684+ DateTime,
685+ Int,
686+ Reference,
687+ Store,
688+ Unicode,
689 )
690 from zope.interface import implementer
691
692@@ -18,32 +22,41 @@ from lp.services.database.constants import (
693 DEFAULT,
694 UTC_NOW,
695 )
696-from lp.services.database.datetimecol import UtcDateTimeCol
697-from lp.services.database.enumcol import EnumCol
698-from lp.services.database.sqlbase import SQLBase
699+from lp.services.database.enumcol import DBEnum
700+from lp.services.database.stormbase import StormBase
701
702
703 @implementer(ISprintSpecification)
704-class SprintSpecification(SQLBase):
705+class SprintSpecification(StormBase):
706 """A link between a sprint and a specification."""
707
708- _table = 'SprintSpecification'
709+ __storm_table__ = 'SprintSpecification'
710
711- sprint = ForeignKey(dbName='sprint', foreignKey='Sprint',
712- notNull=True)
713- specification = ForeignKey(dbName='specification',
714- foreignKey='Specification', notNull=True)
715- status = EnumCol(schema=SprintSpecificationStatus, notNull=True,
716+ id = Int(primary=True)
717+
718+ sprint_id = Int(name='sprint', allow_none=False)
719+ sprint = Reference(sprint_id, 'Sprint.id')
720+ specification_id = Int(name='specification', allow_none=False)
721+ specification = Reference(specification_id, 'Specification.id')
722+ status = DBEnum(
723+ enum=SprintSpecificationStatus, allow_none=False,
724 default=SprintSpecificationStatus.PROPOSED)
725- whiteboard = StringCol(notNull=False, default=None)
726- registrant = ForeignKey(
727- dbName='registrant', foreignKey='Person',
728- storm_validator=validate_public_person, notNull=True)
729- date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)
730- decider = ForeignKey(
731- dbName='decider', foreignKey='Person',
732- storm_validator=validate_public_person, notNull=False, default=None)
733- date_decided = UtcDateTimeCol(notNull=False, default=None)
734+ whiteboard = Unicode(allow_none=True, default=None)
735+ registrant_id = Int(
736+ name='registrant', validator=validate_public_person, allow_none=False)
737+ registrant = Reference(registrant_id, 'Person.id')
738+ date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
739+ decider_id = Int(
740+ name='decider', validator=validate_public_person, allow_none=True,
741+ default=None)
742+ decider = Reference(decider_id, 'Person.id')
743+ date_decided = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
744+
745+ def __init__(self, sprint, specification, registrant):
746+ super(SprintSpecification, self).__init__()
747+ self.sprint = sprint
748+ self.specification = specification
749+ self.registrant = registrant
750
751 @property
752 def is_confirmed(self):
753@@ -66,3 +79,6 @@ class SprintSpecification(SQLBase):
754 self.status = SprintSpecificationStatus.DECLINED
755 self.decider = decider
756 self.date_decided = UTC_NOW
757+
758+ def destroySelf(self):
759+ Store.of(self).remove(self)
760diff --git a/lib/lp/blueprints/vocabularies/sprint.py b/lib/lp/blueprints/vocabularies/sprint.py
761index f300b62..f98df43 100644
762--- a/lib/lp/blueprints/vocabularies/sprint.py
763+++ b/lib/lp/blueprints/vocabularies/sprint.py
764@@ -9,21 +9,17 @@ __all__ = [
765 'SprintVocabulary',
766 ]
767
768-
769 from lp.blueprints.model.sprint import Sprint
770-from lp.services.webapp.vocabulary import NamedSQLObjectVocabulary
771+from lp.services.database.constants import UTC_NOW
772+from lp.services.webapp.vocabulary import NamedStormVocabulary
773
774
775-class FutureSprintVocabulary(NamedSQLObjectVocabulary):
776+class FutureSprintVocabulary(NamedStormVocabulary):
777 """A vocab of all sprints that have not yet finished."""
778
779 _table = Sprint
780-
781- def __iter__(self):
782- future_sprints = Sprint.select("time_ends > 'NOW'")
783- for sprint in future_sprints:
784- yield(self.toTerm(sprint))
785+ _clauses = [Sprint.time_ends > UTC_NOW]
786
787
788-class SprintVocabulary(NamedSQLObjectVocabulary):
789+class SprintVocabulary(NamedStormVocabulary):
790 _table = Sprint
791diff --git a/lib/lp/code/mail/tests/test_codehandler.py b/lib/lp/code/mail/tests/test_codehandler.py
792index a02cc4d..97feda0 100644
793--- a/lib/lp/code/mail/tests/test_codehandler.py
794+++ b/lib/lp/code/mail/tests/test_codehandler.py
795@@ -3,6 +3,8 @@
796
797 """Testing the CodeHandler."""
798
799+from __future__ import absolute_import, print_function, unicode_literals
800+
801 __metaclass__ = type
802
803 from textwrap import dedent
804diff --git a/lib/lp/code/mail/tests/test_codereviewcomment.py b/lib/lp/code/mail/tests/test_codereviewcomment.py
805index 930753e..a692a97 100644
806--- a/lib/lp/code/mail/tests/test_codereviewcomment.py
807+++ b/lib/lp/code/mail/tests/test_codereviewcomment.py
808@@ -243,7 +243,7 @@ class TestCodeReviewComment(TestCaseWithFactory):
809 def test_generateEmailWithVoteAndTag(self):
810 """Ensure that vote tags are displayed."""
811 mailer, subscriber = self.makeMailer(
812- vote=CodeReviewVote.APPROVE, vote_tag='DBTAG')
813+ vote=CodeReviewVote.APPROVE, vote_tag=u'DBTAG')
814 ctrl = mailer.generateEmail(
815 subscriber.preferredemail.email, subscriber)
816 self.assertEqual('Review: Approve dbtag', ctrl.body.splitlines()[0])
817diff --git a/lib/lp/code/model/branchcollection.py b/lib/lp/code/model/branchcollection.py
818index 7482778..bc3a026 100644
819--- a/lib/lp/code/model/branchcollection.py
820+++ b/lib/lp/code/model/branchcollection.py
821@@ -484,10 +484,10 @@ class GenericBranchCollection:
822 tables = [
823 BranchMergeProposal,
824 Join(CodeReviewVoteReference,
825- CodeReviewVoteReference.branch_merge_proposalID == \
826+ CodeReviewVoteReference.branch_merge_proposal ==
827 BranchMergeProposal.id),
828 LeftJoin(CodeReviewComment,
829- CodeReviewVoteReference.commentID == CodeReviewComment.id)]
830+ CodeReviewVoteReference.comment == CodeReviewComment.id)]
831
832 expressions = [
833 CodeReviewVoteReference.reviewer == reviewer,
834diff --git a/lib/lp/code/model/branchmergeproposal.py b/lib/lp/code/model/branchmergeproposal.py
835index 338cc29..2a346b4 100644
836--- a/lib/lp/code/model/branchmergeproposal.py
837+++ b/lib/lp/code/model/branchmergeproposal.py
838@@ -46,6 +46,7 @@ from zope.interface import implementer
839 from zope.security.interfaces import Unauthorized
840
841 from lp.app.enums import PRIVATE_INFORMATION_TYPES
842+from lp.app.errors import NotFoundError
843 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
844 from lp.bugs.interfaces.bugtask import IBugTaskSet
845 from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context
846@@ -565,11 +566,14 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
847 @property
848 def all_comments(self):
849 """See `IBranchMergeProposal`."""
850- return CodeReviewComment.selectBy(branch_merge_proposal=self.id)
851+ return IStore(CodeReviewComment).find(
852+ CodeReviewComment, branch_merge_proposal=self)
853
854 def getComment(self, id):
855 """See `IBranchMergeProposal`."""
856- comment = CodeReviewComment.get(id)
857+ comment = IStore(CodeReviewComment).get(CodeReviewComment, id)
858+ if comment is None:
859+ raise NotFoundError(id)
860 if comment.branch_merge_proposal != self:
861 raise WrongBranchMergeProposal
862 return comment
863@@ -583,7 +587,10 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
864
865 def setCommentVisibility(self, user, comment_number, visible):
866 """See `IBranchMergeProposal`."""
867- comment = CodeReviewComment.get(comment_number)
868+ comment = IStore(CodeReviewComment).get(
869+ CodeReviewComment, comment_number)
870+ if comment is None:
871+ raise NotFoundError(comment_number)
872 if comment.branch_merge_proposal != self:
873 raise WrongBranchMergeProposal
874 if not comment.userCanSetCommentVisibility(user):
875@@ -596,7 +603,9 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
876 """See `IBranchMergeProposal`.
877
878 This function can raise WrongBranchMergeProposal."""
879- vote = CodeReviewVoteReference.get(id)
880+ vote = IStore(CodeReviewVoteReference).get(CodeReviewVoteReference, id)
881+ if vote is None:
882+ raise NotFoundError(id)
883 if vote.branch_merge_proposal != self:
884 raise WrongBranchMergeProposal
885 return vote
886@@ -932,6 +941,7 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
887 date_created=_date_created)
888 self._ensureAssociatedBranchesVisibleToReviewer(reviewer)
889 vote_reference.review_type = review_type
890+ Store.of(vote_reference).flush()
891 if _notify_listeners:
892 notify(ReviewerNominatedEvent(vote_reference))
893 return vote_reference
894@@ -1098,11 +1108,13 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
895 if team_ref is not None:
896 return team_ref
897 # Create a new reference.
898- return CodeReviewVoteReference(
899+ vote_reference = CodeReviewVoteReference(
900 branch_merge_proposal=self,
901 registrant=user,
902 reviewer=user,
903 review_type=review_type)
904+ Store.of(vote_reference).flush()
905+ return vote_reference
906
907 def createCommentFromMessage(self, message, vote, review_type,
908 original_email, _notify_listeners=True,
909@@ -1126,6 +1138,7 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
910 vote_reference.reviewer = message.owner
911 vote_reference.review_type = review_type
912 vote_reference.comment = code_review_message
913+ Store.of(code_review_message).flush()
914 if _notify_listeners:
915 notify(ObjectCreatedEvent(code_review_message))
916 return code_review_message
917@@ -1389,15 +1402,15 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
918 if include_votes:
919 votes = load_referencing(
920 CodeReviewVoteReference, branch_merge_proposals,
921- ['branch_merge_proposalID'])
922+ ['branch_merge_proposal_id'])
923 votes_map = defaultdict(list)
924 for vote in votes:
925- votes_map[vote.branch_merge_proposalID].append(vote)
926+ votes_map[vote.branch_merge_proposal_id].append(vote)
927 for mp in branch_merge_proposals:
928 get_property_cache(mp).votes = votes_map[mp.id]
929- comments = load_related(CodeReviewComment, votes, ['commentID'])
930- load_related(Message, comments, ['messageID'])
931- person_ids.update(vote.reviewerID for vote in votes)
932+ comments = load_related(CodeReviewComment, votes, ['comment_id'])
933+ load_related(Message, comments, ['message_id'])
934+ person_ids.update(vote.reviewer_id for vote in votes)
935
936 # we also provide a summary of diffs, so load them
937 load_related(LibraryFileAlias, diffs, ['diff_textID'])
938@@ -1439,8 +1452,8 @@ class BranchMergeProposalGetter:
939 BranchMergeProposal.registrantID == participant.id)
940
941 review_select = Select(
942- [CodeReviewVoteReference.branch_merge_proposalID],
943- [CodeReviewVoteReference.reviewerID == participant.id])
944+ [CodeReviewVoteReference.branch_merge_proposal_id],
945+ [CodeReviewVoteReference.reviewer == participant])
946
947 query = Store.of(participant).find(
948 BranchMergeProposal,
949@@ -1463,13 +1476,13 @@ class BranchMergeProposalGetter:
950 # the actual vote for that person.
951 tables = [
952 CodeReviewVoteReference,
953- Join(Person, CodeReviewVoteReference.reviewerID == Person.id),
954+ Join(Person, CodeReviewVoteReference.reviewer == Person.id),
955 LeftJoin(
956 CodeReviewComment,
957- CodeReviewVoteReference.commentID == CodeReviewComment.id)]
958+ CodeReviewVoteReference.comment == CodeReviewComment.id)]
959 results = store.using(*tables).find(
960 (CodeReviewVoteReference, Person, CodeReviewComment),
961- CodeReviewVoteReference.branch_merge_proposalID.is_in(ids))
962+ CodeReviewVoteReference.branch_merge_proposal_id.is_in(ids))
963 for reference, person, comment in results:
964 result[reference.branch_merge_proposal].append(reference)
965 return result
966diff --git a/lib/lp/code/model/codereviewcomment.py b/lib/lp/code/model/codereviewcomment.py
967index 47f640c..1029b4e 100644
968--- a/lib/lp/code/model/codereviewcomment.py
969+++ b/lib/lp/code/model/codereviewcomment.py
970@@ -10,9 +10,11 @@ __all__ = [
971
972 from textwrap import TextWrapper
973
974-from sqlobject import (
975- ForeignKey,
976- StringCol,
977+from storm.locals import (
978+ Int,
979+ Reference,
980+ Store,
981+ Unicode,
982 )
983 from zope.interface import implementer
984
985@@ -22,8 +24,8 @@ from lp.code.interfaces.codereviewcomment import (
986 ICodeReviewComment,
987 ICodeReviewCommentDeletion,
988 )
989-from lp.services.database.enumcol import EnumCol
990-from lp.services.database.sqlbase import SQLBase
991+from lp.services.database.enumcol import DBEnum
992+from lp.services.database.stormbase import StormBase
993 from lp.services.mail.signedmessage import signed_message_from_string
994
995
996@@ -60,17 +62,27 @@ def quote_text_as_email(text, width=80):
997
998
999 @implementer(ICodeReviewComment, ICodeReviewCommentDeletion, IHasBranchTarget)
1000-class CodeReviewComment(SQLBase):
1001+class CodeReviewComment(StormBase):
1002 """A table linking branch merge proposals and messages."""
1003
1004- _table = 'CodeReviewMessage'
1005-
1006- branch_merge_proposal = ForeignKey(
1007- dbName='branch_merge_proposal', foreignKey='BranchMergeProposal',
1008- notNull=True)
1009- message = ForeignKey(dbName='message', foreignKey='Message', notNull=True)
1010- vote = EnumCol(dbName='vote', notNull=False, schema=CodeReviewVote)
1011- vote_tag = StringCol(default=None)
1012+ __storm_table__ = 'CodeReviewMessage'
1013+
1014+ id = Int(primary=True)
1015+ branch_merge_proposal_id = Int(
1016+ name='branch_merge_proposal', allow_none=False)
1017+ branch_merge_proposal = Reference(
1018+ branch_merge_proposal_id, 'BranchMergeProposal.id')
1019+ message_id = Int(name='message', allow_none=False)
1020+ message = Reference(message_id, 'Message.id')
1021+ vote = DBEnum(name='vote', allow_none=True, enum=CodeReviewVote)
1022+ vote_tag = Unicode(default=None)
1023+
1024+ def __init__(self, branch_merge_proposal, message, vote=None,
1025+ vote_tag=None):
1026+ self.branch_merge_proposal = branch_merge_proposal
1027+ self.message = message
1028+ self.vote = vote
1029+ self.vote_tag = vote_tag
1030
1031 @property
1032 def author(self):
1033@@ -134,3 +146,7 @@ class CodeReviewComment(SQLBase):
1034 return (
1035 self.branch_merge_proposal.userCanSetCommentVisibility(user) or
1036 (user is not None and user.inTeam(self.author)))
1037+
1038+ def destroySelf(self):
1039+ """Delete this comment."""
1040+ Store.of(self).remove(self)
1041diff --git a/lib/lp/code/model/codereviewvote.py b/lib/lp/code/model/codereviewvote.py
1042index d2e5c53..b695ebe 100644
1043--- a/lib/lp/code/model/codereviewvote.py
1044+++ b/lib/lp/code/model/codereviewvote.py
1045@@ -8,12 +8,15 @@ __all__ = [
1046 'CodeReviewVoteReference',
1047 ]
1048
1049-from sqlobject import (
1050- ForeignKey,
1051- StringCol,
1052+import pytz
1053+from storm.locals import (
1054+ DateTime,
1055+ Int,
1056+ Reference,
1057+ Store,
1058+ Unicode,
1059 )
1060 from zope.interface import implementer
1061-from zope.schema import Int
1062
1063 from lp.code.errors import (
1064 ClaimReviewFailed,
1065@@ -22,27 +25,36 @@ from lp.code.errors import (
1066 )
1067 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
1068 from lp.services.database.constants import DEFAULT
1069-from lp.services.database.datetimecol import UtcDateTimeCol
1070-from lp.services.database.sqlbase import SQLBase
1071+from lp.services.database.stormbase import StormBase
1072
1073
1074 @implementer(ICodeReviewVoteReference)
1075-class CodeReviewVoteReference(SQLBase):
1076+class CodeReviewVoteReference(StormBase):
1077 """See `ICodeReviewVote`"""
1078
1079- _table = 'CodeReviewVote'
1080- id = Int()
1081- branch_merge_proposal = ForeignKey(
1082- dbName='branch_merge_proposal', foreignKey='BranchMergeProposal',
1083- notNull=True)
1084- date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)
1085- registrant = ForeignKey(
1086- dbName='registrant', foreignKey='Person', notNull=True)
1087- reviewer = ForeignKey(
1088- dbName='reviewer', foreignKey='Person', notNull=True)
1089- review_type = StringCol(default=None)
1090- comment = ForeignKey(
1091- dbName='vote_message', foreignKey='CodeReviewComment', default=None)
1092+ __storm_table__ = 'CodeReviewVote'
1093+
1094+ id = Int(primary=True)
1095+ branch_merge_proposal_id = Int(
1096+ name='branch_merge_proposal', allow_none=False)
1097+ branch_merge_proposal = Reference(
1098+ branch_merge_proposal_id, 'BranchMergeProposal.id')
1099+ date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
1100+ registrant_id = Int(name='registrant', allow_none=False)
1101+ registrant = Reference(registrant_id, 'Person.id')
1102+ reviewer_id = Int(name='reviewer', allow_none=False)
1103+ reviewer = Reference(reviewer_id, 'Person.id')
1104+ review_type = Unicode(default=None)
1105+ comment_id = Int(name='vote_message', default=None)
1106+ comment = Reference(comment_id, 'CodeReviewComment.id')
1107+
1108+ def __init__(self, branch_merge_proposal, registrant, reviewer,
1109+ review_type=None, date_created=DEFAULT):
1110+ self.branch_merge_proposal = branch_merge_proposal
1111+ self.registrant = registrant
1112+ self.reviewer = reviewer
1113+ self.review_type = review_type
1114+ self.date_created = date_created
1115
1116 @property
1117 def is_pending(self):
1118@@ -96,6 +108,10 @@ class CodeReviewVoteReference(SQLBase):
1119 self.validateReasignReview(reviewer)
1120 self.reviewer = reviewer
1121
1122+ def destroySelf(self):
1123+ """Delete this vote."""
1124+ Store.of(self).remove(self)
1125+
1126 def delete(self):
1127 """See `ICodeReviewVote`"""
1128 if not self.is_pending:
1129diff --git a/lib/lp/code/model/gitcollection.py b/lib/lp/code/model/gitcollection.py
1130index 4095d5e..6447d0b 100644
1131--- a/lib/lp/code/model/gitcollection.py
1132+++ b/lib/lp/code/model/gitcollection.py
1133@@ -412,10 +412,10 @@ class GenericGitCollection:
1134 tables = [
1135 BranchMergeProposal,
1136 Join(CodeReviewVoteReference,
1137- CodeReviewVoteReference.branch_merge_proposalID == \
1138+ CodeReviewVoteReference.branch_merge_proposal ==
1139 BranchMergeProposal.id),
1140 LeftJoin(CodeReviewComment,
1141- CodeReviewVoteReference.commentID == CodeReviewComment.id)]
1142+ CodeReviewVoteReference.comment == CodeReviewComment.id)]
1143
1144 expressions = [
1145 CodeReviewVoteReference.reviewer == reviewer,
1146diff --git a/lib/lp/code/model/tests/test_branch.py b/lib/lp/code/model/tests/test_branch.py
1147index 1978e88..e2421ba 100644
1148--- a/lib/lp/code/model/tests/test_branch.py
1149+++ b/lib/lp/code/model/tests/test_branch.py
1150@@ -1566,8 +1566,8 @@ class TestBranchDeletionConsequences(TestCase):
1151 comment_id = comment.id
1152 branch = comment.branch_merge_proposal.source_branch
1153 branch.destroySelf(break_references=True)
1154- self.assertRaises(
1155- SQLObjectNotFound, CodeReviewComment.get, comment_id)
1156+ self.assertIsNone(
1157+ IStore(CodeReviewComment).get(CodeReviewComment, comment_id))
1158
1159 def test_deleteTargetCodeReviewComment(self):
1160 """Deletion of branches that have CodeReviewComments works."""
1161@@ -1575,8 +1575,8 @@ class TestBranchDeletionConsequences(TestCase):
1162 comment_id = comment.id
1163 branch = comment.branch_merge_proposal.target_branch
1164 branch.destroySelf(break_references=True)
1165- self.assertRaises(
1166- SQLObjectNotFound, CodeReviewComment.get, comment_id)
1167+ self.assertIsNone(
1168+ IStore(CodeReviewComment).get(CodeReviewComment, comment_id))
1169
1170 def test_branchWithBugRequirements(self):
1171 """Deletion requirements for a branch with a bug are right."""
1172diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
1173index a8b9c35..62bf865 100644
1174--- a/lib/lp/code/model/tests/test_gitrepository.py
1175+++ b/lib/lp/code/model/tests/test_gitrepository.py
1176@@ -1086,8 +1086,8 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
1177 comment_id = comment.id
1178 repository = comment.branch_merge_proposal.source_git_repository
1179 repository.destroySelf(break_references=True)
1180- self.assertRaises(
1181- SQLObjectNotFound, CodeReviewComment.get, comment_id)
1182+ self.assertIsNone(
1183+ IStore(CodeReviewComment).get(CodeReviewComment, comment_id))
1184
1185 def test_delete_target_CodeReviewComment(self):
1186 # Deletion of target repositories that have CodeReviewComments works.
1187@@ -1095,8 +1095,8 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
1188 comment_id = comment.id
1189 repository = comment.branch_merge_proposal.target_git_repository
1190 repository.destroySelf(break_references=True)
1191- self.assertRaises(
1192- SQLObjectNotFound, CodeReviewComment.get, comment_id)
1193+ self.assertIsNone(
1194+ IStore(CodeReviewComment).get(CodeReviewComment, comment_id))
1195
1196 def test_sourceBranchWithCodeReviewVoteReference(self):
1197 # break_references handles CodeReviewVoteReference source repository.
1198diff --git a/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt b/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
1199index 1580ea4..98483a4 100644
1200--- a/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
1201+++ b/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
1202@@ -463,7 +463,7 @@ which is the one we want the method to return.
1203 ... product=blob, set_state=BranchMergeProposalStatus.NEEDS_REVIEW,
1204 ... registrant=branch_owner, source_branch=source_branch)
1205 >>> proposal.nominateReviewer(target_owner, branch_owner)
1206- <CodeReviewVoteReference at ...>
1207+ <lp.code.model.codereviewvote.CodeReviewVoteReference object at ...>
1208
1209 And then we propose a merge the other way, so that the owner is target,
1210 but they have not been asked to review, meaning that the method shouldn't
1211@@ -474,7 +474,7 @@ return this review.
1212 ... product=blob, set_state=BranchMergeProposalStatus.NEEDS_REVIEW,
1213 ... registrant=target_owner, source_branch=target_branch)
1214 >>> proposal.nominateReviewer(branch_owner, target_owner)
1215- <CodeReviewVoteReference at ...>
1216+ <lp.code.model.codereviewvote.CodeReviewVoteReference object at ...>
1217 >>> logout()
1218
1219 >>> proposals = webservice.named_get('/~target', 'getRequestedReviews'
1220diff --git a/lib/lp/codehosting/puller/tests/test_scheduler.py b/lib/lp/codehosting/puller/tests/test_scheduler.py
1221index 2b08509..34d53b6 100644
1222--- a/lib/lp/codehosting/puller/tests/test_scheduler.py
1223+++ b/lib/lp/codehosting/puller/tests/test_scheduler.py
1224@@ -553,7 +553,7 @@ class TestPullerMasterIntegration(PullerBranchTestCase):
1225 """Tests for the puller master that launch sub-processes."""
1226
1227 layer = ZopelessAppServerLayer
1228- run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
1229+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
1230
1231 def setUp(self):
1232 super(TestPullerMasterIntegration, self).setUp()
1233diff --git a/lib/lp/registry/model/projectgroup.py b/lib/lp/registry/model/projectgroup.py
1234index 8a22fa8..bc6c5e5 100644
1235--- a/lib/lp/registry/model/projectgroup.py
1236+++ b/lib/lp/registry/model/projectgroup.py
1237@@ -50,7 +50,11 @@ from lp.blueprints.model.specification import (
1238 Specification,
1239 )
1240 from lp.blueprints.model.specificationsearch import search_specifications
1241-from lp.blueprints.model.sprint import HasSprintsMixin
1242+from lp.blueprints.model.sprint import (
1243+ HasSprintsMixin,
1244+ Sprint,
1245+ )
1246+from lp.blueprints.model.sprintspecification import SprintSpecification
1247 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
1248 from lp.bugs.model.bugtarget import (
1249 BugTargetBase,
1250@@ -239,15 +243,14 @@ class ProjectGroup(SQLBase, BugTargetBase, HasSpecificationsMixin,
1251 """ See `IProjectGroup`."""
1252 return not self.getBranches().is_empty()
1253
1254- def _getBaseQueryAndClauseTablesForQueryingSprints(self):
1255- query = """
1256- Product.project = %s
1257- AND Specification.product = Product.id
1258- AND Specification.id = SprintSpecification.specification
1259- AND SprintSpecification.sprint = Sprint.id
1260- AND SprintSpecification.status = %s
1261- """ % sqlvalues(self, SprintSpecificationStatus.ACCEPTED)
1262- return query, ['Product', 'Specification', 'SprintSpecification']
1263+ def _getBaseClausesForQueryingSprints(self):
1264+ return [
1265+ Product.projectgroup == self,
1266+ Specification.product == Product.id,
1267+ Specification.id == SprintSpecification.specification_id,
1268+ SprintSpecification.sprint == Sprint.id,
1269+ SprintSpecification.status == SprintSpecificationStatus.ACCEPTED,
1270+ ]
1271
1272 def specifications(self, user, sort=None, quantity=None, filter=None,
1273 series=None, need_people=True, need_branches=True,
1274diff --git a/lib/lp/services/database/policy.py b/lib/lp/services/database/policy.py
1275index 066fdb3..691baf4 100644
1276--- a/lib/lp/services/database/policy.py
1277+++ b/lib/lp/services/database/policy.py
1278@@ -358,7 +358,7 @@ class LaunchpadDatabasePolicy(BaseDatabasePolicy):
1279 slave_store = self.getStore(MAIN_STORE, SLAVE_FLAVOR)
1280 hot_standby, streaming_lag = slave_store.execute("""
1281 SELECT
1282- current_setting('hot_standby') = 'on',
1283+ pg_is_in_recovery(),
1284 now() - pg_last_xact_replay_timestamp()
1285 """).get_one()
1286 if hot_standby and streaming_lag is not None:
1287diff --git a/lib/lp/services/gpg/handler.py b/lib/lp/services/gpg/handler.py
1288index b2bcbad..e3eba45 100644
1289--- a/lib/lp/services/gpg/handler.py
1290+++ b/lib/lp/services/gpg/handler.py
1291@@ -489,12 +489,8 @@ class GPGHandler:
1292 raise GPGKeyExpired(key)
1293 return key
1294
1295- def _submitKey(self, content):
1296- """Submit an ASCII-armored public key export to the keyserver.
1297-
1298- It issues a POST at /pks/add on the keyserver specified in the
1299- configuration.
1300- """
1301+ def submitKey(self, content):
1302+ """See `IGPGHandler`."""
1303 keyserver_http_url = '%s:%s' % (
1304 config.gpghandler.host, config.gpghandler.port)
1305
1306@@ -527,7 +523,7 @@ class GPGHandler:
1307 return
1308
1309 pub_key = self.retrieveKey(fingerprint)
1310- self._submitKey(pub_key.export())
1311+ self.submitKey(pub_key.export())
1312
1313 def getURLForKeyInServer(self, fingerprint, action='index', public=False):
1314 """See IGPGHandler"""
1315diff --git a/lib/lp/services/gpg/interfaces.py b/lib/lp/services/gpg/interfaces.py
1316index 78b44c8..d6f0f73 100644
1317--- a/lib/lp/services/gpg/interfaces.py
1318+++ b/lib/lp/services/gpg/interfaces.py
1319@@ -357,6 +357,17 @@ class IGPGHandler(Interface):
1320 :return: a `PymeKey`object containing the key information.
1321 """
1322
1323+ def submitKey(content):
1324+ """Submit an ASCII-armored public key export to the keyserver.
1325+
1326+ It issues a POST at /pks/add on the keyserver specified in the
1327+ configuration.
1328+
1329+ :param content: The exported public key, as a byte string.
1330+ :raise GPGUploadFailure: if the keyserver could not be reached.
1331+ :raise AssertionError: if the POST request failed.
1332+ """
1333+
1334 def uploadPublicKey(fingerprint):
1335 """Upload the specified public key to a keyserver.
1336
1337@@ -365,8 +376,8 @@ class IGPGHandler(Interface):
1338
1339 :param fingerprint: The key fingerprint, which must be an hexadecimal
1340 string.
1341- :raise GPGUploadFailure: if the keyserver could not be reaches.
1342- :raise AssertionError: if the POST request doesn't succeed.
1343+ :raise GPGUploadFailure: if the keyserver could not be reached.
1344+ :raise AssertionError: if the POST request failed.
1345 """
1346
1347 def localKeys(filter=None, secret=False):
1348diff --git a/lib/lp/services/librarianserver/tests/test_storage_db.py b/lib/lp/services/librarianserver/tests/test_storage_db.py
1349index 47fe847..b24d87b 100644
1350--- a/lib/lp/services/librarianserver/tests/test_storage_db.py
1351+++ b/lib/lp/services/librarianserver/tests/test_storage_db.py
1352@@ -146,7 +146,7 @@ class LibrarianStorageDBTests(TestCase):
1353 class LibrarianStorageSwiftTests(TestCase):
1354
1355 layer = LaunchpadZopelessLayer
1356- run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
1357+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
1358
1359 def setUp(self):
1360 super(LibrarianStorageSwiftTests, self).setUp()
1361diff --git a/lib/lp/services/mail/helpers.py b/lib/lp/services/mail/helpers.py
1362index 82640aa..80f68d3 100644
1363--- a/lib/lp/services/mail/helpers.py
1364+++ b/lib/lp/services/mail/helpers.py
1365@@ -35,7 +35,13 @@ class IncomingEmailError(Exception):
1366
1367
1368 def get_main_body(signed_msg):
1369- """Returns the first text part of the email."""
1370+ """Returns the first text part of the email.
1371+
1372+ This always returns text (or None if the email has no text parts at
1373+ all). It decodes using the character set in the text part's
1374+ Content-Type, or ISO-8859-1 if unspecified (in order to minimise the
1375+ chances of `UnicodeDecodeError`s).
1376+ """
1377 msg = getattr(signed_msg, 'signedMessage', None)
1378 if msg is None:
1379 # The email wasn't signed.
1380@@ -43,9 +49,11 @@ def get_main_body(signed_msg):
1381 if msg.is_multipart():
1382 for part in msg.walk():
1383 if part.get_content_type() == 'text/plain':
1384- return part.get_payload(decode=True)
1385+ charset = part.get_content_charset('ISO-8859-1')
1386+ return part.get_payload(decode=True).decode(charset)
1387 else:
1388- return msg.get_payload(decode=True)
1389+ charset = msg.get_content_charset('ISO-8859-1')
1390+ return msg.get_payload(decode=True).decode(charset)
1391
1392
1393 def guess_bugtask(bug, person):
1394diff --git a/lib/lp/services/signing/tests/helpers.py b/lib/lp/services/signing/tests/helpers.py
1395index f819745..6831edb 100644
1396--- a/lib/lp/services/signing/tests/helpers.py
1397+++ b/lib/lp/services/signing/tests/helpers.py
1398@@ -49,7 +49,7 @@ class SigningServiceClientFixture(fixtures.Fixture):
1399 openpgp_key_algorithm=None, length=None):
1400 key = bytes(PrivateKey.generate().public_key)
1401 data = {
1402- "fingerprint": self.factory.getUniqueHexString(40),
1403+ "fingerprint": self.factory.getUniqueHexString(40).upper(),
1404 "public-key": key,
1405 }
1406 self.generate_returns.append((key_type, data))
1407@@ -69,7 +69,7 @@ class SigningServiceClientFixture(fixtures.Fixture):
1408
1409 def _inject(self, key_type, private_key, public_key, description,
1410 created_at):
1411- data = {'fingerprint': self.factory.getUniqueHexString(40)}
1412+ data = {'fingerprint': self.factory.getUniqueHexString(40).upper()}
1413 self.inject_returns.append(data)
1414 return data
1415
1416diff --git a/lib/lp/services/worlddata/vocabularies.py b/lib/lp/services/worlddata/vocabularies.py
1417index 58d2c8e..be963f4 100644
1418--- a/lib/lp/services/worlddata/vocabularies.py
1419+++ b/lib/lp/services/worlddata/vocabularies.py
1420@@ -1,6 +1,8 @@
1421 # Copyright 2009 Canonical Ltd. This software is licensed under the
1422 # GNU Affero General Public License version 3 (see the file LICENSE).
1423
1424+from __future__ import absolute_import, print_function, unicode_literals
1425+
1426 __all__ = [
1427 'CountryNameVocabulary',
1428 'LanguageVocabulary',
1429@@ -10,6 +12,7 @@ __all__ = [
1430 __metaclass__ = type
1431
1432 import pytz
1433+import six
1434 from sqlobject import SQLObjectNotFound
1435 from zope.interface import alsoProvides
1436 from zope.schema.vocabulary import (
1437@@ -24,7 +27,7 @@ from lp.services.worlddata.model.country import Country
1438 from lp.services.worlddata.model.language import Language
1439
1440 # create a sorted list of the common time zone names, with UTC at the start
1441-_values = sorted(pytz.common_timezones)
1442+_values = sorted(six.ensure_text(tz) for tz in pytz.common_timezones)
1443 _values.remove('UTC')
1444 _values.insert(0, 'UTC')
1445
1446diff --git a/lib/lp/soyuz/adapters/tests/test_archivedependencies.py b/lib/lp/soyuz/adapters/tests/test_archivedependencies.py
1447index 7cd3cad..d5a2068 100644
1448--- a/lib/lp/soyuz/adapters/tests/test_archivedependencies.py
1449+++ b/lib/lp/soyuz/adapters/tests/test_archivedependencies.py
1450@@ -128,7 +128,7 @@ class TestSourcesList(TestCaseWithFactory):
1451 """Test sources.list contents for building, and related mechanisms."""
1452
1453 layer = LaunchpadZopelessLayer
1454- run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
1455+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
1456
1457 ubuntu_components = [
1458 "main", "restricted", "universe", "multiverse", "partner"]
1459diff --git a/lib/lp/soyuz/configure.zcml b/lib/lp/soyuz/configure.zcml
1460index 2e97958..643a85b 100644
1461--- a/lib/lp/soyuz/configure.zcml
1462+++ b/lib/lp/soyuz/configure.zcml
1463@@ -373,7 +373,6 @@
1464 set_schema="lp.soyuz.interfaces.archive.IArchiveRestricted"/>
1465 <require
1466 permission="launchpad.InternalScriptsOnly"
1467- attributes="signing_key_owner"
1468 set_attributes="dirty_suites distribution signing_key_owner
1469 signing_key_fingerprint"/>
1470 </class>
1471diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py
1472index 407f953..b6cc663 100644
1473--- a/lib/lp/soyuz/interfaces/archive.py
1474+++ b/lib/lp/soyuz/interfaces/archive.py
1475@@ -461,6 +461,8 @@ class IArchiveSubscriberView(Interface):
1476 "explicit publish flag and any other constraints."))
1477 series_with_sources = Attribute(
1478 "DistroSeries to which this archive has published sources")
1479+ signing_key_owner = Reference(
1480+ title=_("Archive signing key owner"), required=False, schema=IPerson)
1481 signing_key_fingerprint = exported(
1482 Text(
1483 title=_("Archive signing key fingerprint"), required=False,
1484diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py
1485index a37b737..0d58e56 100644
1486--- a/lib/lp/soyuz/model/archive.py
1487+++ b/lib/lp/soyuz/model/archive.py
1488@@ -466,7 +466,7 @@ class Archive(SQLBase):
1489 return (
1490 not config.personalpackagearchive.require_signing_keys or
1491 not self.is_ppa or
1492- self.signing_key is not None)
1493+ self.signing_key_fingerprint is not None)
1494
1495 @property
1496 def reference(self):
1497@@ -2717,10 +2717,12 @@ class ArchiveSet:
1498 (owner.name, distribution.name, name))
1499
1500 # Signing-key for the default PPA is reused when it's already present.
1501- signing_key = None
1502+ signing_key_owner = None
1503+ signing_key_fingerprint = None
1504 if purpose == ArchivePurpose.PPA:
1505 if owner.archive is not None:
1506- signing_key = owner.archive.signing_key
1507+ signing_key_owner = owner.archive.signing_key_owner
1508+ signing_key_fingerprint = owner.archive.signing_key_fingerprint
1509 else:
1510 # owner.archive is a cached property and we've just cached it.
1511 del get_property_cache(owner).archive
1512@@ -2729,9 +2731,8 @@ class ArchiveSet:
1513 owner=owner, distribution=distribution, name=name,
1514 displayname=displayname, description=description,
1515 purpose=purpose, publish=publish,
1516- signing_key_owner=signing_key.owner if signing_key else None,
1517- signing_key_fingerprint=(
1518- signing_key.fingerprint if signing_key else None),
1519+ signing_key_owner=signing_key_owner,
1520+ signing_key_fingerprint=signing_key_fingerprint,
1521 require_virtualized=require_virtualized)
1522
1523 # Upon creation archives are enabled by default.
1524diff --git a/lib/lp/soyuz/scripts/ppakeygenerator.py b/lib/lp/soyuz/scripts/ppakeygenerator.py
1525index 190b4a0..88e1d84 100644
1526--- a/lib/lp/soyuz/scripts/ppakeygenerator.py
1527+++ b/lib/lp/soyuz/scripts/ppakeygenerator.py
1528@@ -34,7 +34,7 @@ class PPAKeyGenerator(LaunchpadCronScript):
1529 (archive.reference, archive.displayname))
1530 archive_signing_key = IArchiveGPGSigningKey(archive)
1531 archive_signing_key.generateSigningKey(log=self.logger)
1532- self.logger.info("Key %s" % archive.signing_key.fingerprint)
1533+ self.logger.info("Key %s" % archive.signing_key_fingerprint)
1534
1535 def main(self):
1536 """Generate signing keys for the selected PPAs."""
1537@@ -45,11 +45,11 @@ class PPAKeyGenerator(LaunchpadCronScript):
1538 raise LaunchpadScriptFailure(
1539 "No archive named '%s' could be found."
1540 % self.options.archive)
1541- if archive.signing_key is not None:
1542+ if archive.signing_key_fingerprint is not None:
1543 raise LaunchpadScriptFailure(
1544 "%s (%s) already has a signing_key (%s)"
1545 % (archive.reference, archive.displayname,
1546- archive.signing_key.fingerprint))
1547+ archive.signing_key_fingerprint))
1548 archives = [archive]
1549 else:
1550 archive_set = getUtility(IArchiveSet)
1551diff --git a/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py b/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py
1552index 56e8710..a5d3caf 100644
1553--- a/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py
1554+++ b/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py
1555@@ -83,7 +83,7 @@ class TestPPAKeyGenerator(TestCase):
1556 LaunchpadScriptFailure,
1557 ("~cprov/ubuntu/ppa (PPA for Celso Providelo) already has a "
1558 "signing_key (%s)" %
1559- cprov.archive.signing_key.fingerprint),
1560+ cprov.archive.signing_key_fingerprint),
1561 key_generator.main)
1562
1563 def testGenerateKeyForASinglePPA(self):
1564@@ -95,14 +95,14 @@ class TestPPAKeyGenerator(TestCase):
1565 cprov = getUtility(IPersonSet).getByName('cprov')
1566 self._fixArchiveForKeyGeneration(cprov.archive)
1567
1568- self.assertTrue(cprov.archive.signing_key is None)
1569+ self.assertIsNone(cprov.archive.signing_key_fingerprint)
1570
1571 txn = FakeTransaction()
1572 key_generator = self._getKeyGenerator(
1573 archive_reference='~cprov/ubuntutest/ppa', txn=txn)
1574 key_generator.main()
1575
1576- self.assertTrue(cprov.archive.signing_key is not None)
1577+ self.assertIsNotNone(cprov.archive.signing_key_fingerprint)
1578 self.assertEqual(txn.commit_count, 1)
1579
1580 def testGenerateKeyForAllPPA(self):
1581@@ -115,13 +115,13 @@ class TestPPAKeyGenerator(TestCase):
1582
1583 for archive in archives:
1584 self._fixArchiveForKeyGeneration(archive)
1585- self.assertTrue(archive.signing_key is None)
1586+ self.assertIsNone(archive.signing_key_fingerprint)
1587
1588 txn = FakeTransaction()
1589 key_generator = self._getKeyGenerator(txn=txn)
1590 key_generator.main()
1591
1592 for archive in archives:
1593- self.assertTrue(archive.signing_key is not None)
1594+ self.assertIsNotNone(archive.signing_key_fingerprint)
1595
1596 self.assertEqual(txn.commit_count, len(archives))
1597diff --git a/lib/lp/soyuz/stories/soyuz/xx-person-packages.txt b/lib/lp/soyuz/stories/soyuz/xx-person-packages.txt
1598index d815121..95ff407 100644
1599--- a/lib/lp/soyuz/stories/soyuz/xx-person-packages.txt
1600+++ b/lib/lp/soyuz/stories/soyuz/xx-person-packages.txt
1601@@ -400,7 +400,7 @@ Then delete the 'source2' package.
1602 ... print(extract_text(empty_section))
1603 >>> print_ppa_packages(admin_browser.contents)
1604 Source Published Status Series Section Build Status
1605- source2 - 666... a moment ago Deleted ...
1606+ source2 - 666... Deleted ...
1607 >>> update_cached_records()
1608
1609 Now re-list the PPA's packages, 'source2' was deleted but still
1610diff --git a/lib/lp/soyuz/tests/test_archive.py b/lib/lp/soyuz/tests/test_archive.py
1611index 91acf53..5c3f3b6 100644
1612--- a/lib/lp/soyuz/tests/test_archive.py
1613+++ b/lib/lp/soyuz/tests/test_archive.py
1614@@ -4046,7 +4046,7 @@ class TestSigningKeyPropagation(TestCaseWithFactory):
1615
1616 def test_ppa_created_with_no_signing_key(self):
1617 ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
1618- self.assertIsNone(ppa.signing_key)
1619+ self.assertIsNone(ppa.signing_key_fingerprint)
1620
1621 def test_default_signing_key_propagated_to_new_ppa(self):
1622 person = self.factory.makePerson()
1623diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
1624index f5ad237..c109014 100644
1625--- a/lib/lp/testing/factory.py
1626+++ b/lib/lp/testing/factory.py
1627@@ -1121,14 +1121,14 @@ class BareLaunchpadObjectFactory(ObjectFactory):
1628 def makeSprint(self, title=None, name=None):
1629 """Make a sprint."""
1630 if title is None:
1631- title = self.getUniqueString('title')
1632+ title = self.getUniqueUnicode('title')
1633 owner = self.makePerson()
1634 if name is None:
1635- name = self.getUniqueString('name')
1636+ name = self.getUniqueUnicode('name')
1637 time_starts = datetime(2009, 1, 1, tzinfo=pytz.UTC)
1638 time_ends = datetime(2009, 1, 2, tzinfo=pytz.UTC)
1639- time_zone = 'UTC'
1640- summary = self.getUniqueString('summary')
1641+ time_zone = u'UTC'
1642+ summary = self.getUniqueUnicode('summary')
1643 return getUtility(ISprintSet).new(
1644 owner=owner, name=name, title=title, time_zone=time_zone,
1645 time_starts=time_starts, time_ends=time_ends, summary=summary)
1646diff --git a/lib/lp/translations/pottery/tests/test_detect_intltool.py b/lib/lp/translations/pottery/tests/test_detect_intltool.py
1647index cc0d5d7..21dce33 100644
1648--- a/lib/lp/translations/pottery/tests/test_detect_intltool.py
1649+++ b/lib/lp/translations/pottery/tests/test_detect_intltool.py
1650@@ -6,9 +6,11 @@ __metaclass__ = type
1651 import errno
1652 import os
1653 import tarfile
1654+from textwrap import dedent
1655
1656 from breezy.controldir import ControlDir
1657
1658+from lp.services.scripts.tests import run_script
1659 from lp.testing import TestCase
1660 from lp.translations.pottery.detect_intltool import is_intltool_structure
1661
1662@@ -52,6 +54,18 @@ class SetupTestPackageMixin:
1663 with open(path, 'w') as the_file:
1664 the_file.write(content)
1665
1666+ def test_pottery_generate_intltool_script(self):
1667+ # Let the script run to see it works fine.
1668+ self.prepare_package("intltool_POTFILES_in_2")
1669+
1670+ return_code, stdout, stderr = run_script(
1671+ 'scripts/rosetta/pottery-generate-intltool.py', [])
1672+
1673+ self.assertEqual(dedent("""\
1674+ module1/po/messages.pot
1675+ po/messages.pot
1676+ """), stdout)
1677+
1678
1679 class TestDetectIntltoolInBzrTree(TestCase, SetupTestPackageMixin):
1680
1681diff --git a/scripts/rosetta/pottery-generate-intltool.py b/scripts/rosetta/pottery-generate-intltool.py
1682new file mode 100755
1683index 0000000..4557676
1684--- /dev/null
1685+++ b/scripts/rosetta/pottery-generate-intltool.py
1686@@ -0,0 +1,56 @@
1687+#!/usr/bin/python2 -S
1688+#
1689+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
1690+# GNU Affero General Public License version 3 (see the file LICENSE).
1691+
1692+"""Print a list of directories that contain a valid intltool structure."""
1693+
1694+from __future__ import absolute_import, print_function, unicode_literals
1695+
1696+import _pythonpath
1697+
1698+import os.path
1699+
1700+from lpbuildd.pottery.intltool import generate_pots
1701+from lpbuildd.tests.fakeslave import UncontainedBackend as _UncontainedBackend
1702+
1703+from lp.services.scripts.base import LaunchpadScript
1704+
1705+
1706+class UncontainedBackend(_UncontainedBackend):
1707+ """Like UncontainedBackend, except avoid executing "test".
1708+
1709+ Otherwise we can end up with confusion between the Unix "test" utility
1710+ and Launchpad's bin/test.
1711+ """
1712+
1713+ def path_exists(self, path):
1714+ """See `Backend`."""
1715+ return os.path.exists(path)
1716+
1717+ def isdir(self, path):
1718+ """See `Backend`."""
1719+ return os.path.isdir(path)
1720+
1721+ def islink(self, path):
1722+ """See `Backend`."""
1723+ return os.path.islink(path)
1724+
1725+
1726+class PotteryGenerateIntltool(LaunchpadScript):
1727+ """Print a list of directories that contain a valid intltool structure."""
1728+
1729+ def add_my_options(self):
1730+ """See `LaunchpadScript`."""
1731+ self.parser.usage = "%prog [options] [PATH]"
1732+
1733+ def main(self):
1734+ """See `LaunchpadScript`."""
1735+ path = self.args[0] if self.args else "."
1736+ backend = UncontainedBackend("dummy")
1737+ print("\n".join(generate_pots(backend, path)))
1738+
1739+
1740+if __name__ == "__main__":
1741+ script = PotteryGenerateIntltool(name="pottery-generate-intltool")
1742+ script.run()
1743diff --git a/utilities/launchpad-database-setup b/utilities/launchpad-database-setup
1744index 46a36ce..83b79b4 100755
1745--- a/utilities/launchpad-database-setup
1746+++ b/utilities/launchpad-database-setup
1747@@ -43,13 +43,6 @@ if ! sudo grep -q "port.*5432" /etc/postgresql/$pgversion/main/postgresql.conf;
1748 echo "ensure postgres is running on port 5432."
1749 fi;
1750
1751-if [ -e /etc/init.d/postgresql-$pgversion ]; then
1752- sudo /etc/init.d/postgresql-$pgversion stop
1753-else
1754- # This is Maverick.
1755- sudo /etc/init.d/postgresql stop $pgversion
1756-fi
1757-
1758 echo Purging postgresql data...
1759 sudo pg_dropcluster $pgversion main --stop-server
1760 echo Re-creating postgresql database...
1761diff --git a/utilities/sourcedeps.cache b/utilities/sourcedeps.cache
1762index ff07c4c..ca3b453 100644
1763--- a/utilities/sourcedeps.cache
1764+++ b/utilities/sourcedeps.cache
1765@@ -24,8 +24,8 @@
1766 "cjwatson@canonical.com-20190614154330-091l9edcnubsjmsx"
1767 ],
1768 "loggerhead": [
1769- 506,
1770- "cjwatson@canonical.com-20200710095850-o3aa6eo5a22jhuun"
1771+ 511,
1772+ "otto-copilot@canonical.com-20200918084828-dljpy2eewt6umnmd"
1773 ],
1774 "pygettextpo": [
1775 25,
1776diff --git a/utilities/sourcedeps.conf b/utilities/sourcedeps.conf
1777index 2377277..2815420 100644
1778--- a/utilities/sourcedeps.conf
1779+++ b/utilities/sourcedeps.conf
1780@@ -13,5 +13,5 @@ bzr-git lp:~launchpad-pqm/bzr-git/devel;revno=280
1781 bzr-svn lp:~launchpad-pqm/bzr-svn/devel;revno=2725
1782 cscvs lp:~launchpad-pqm/launchpad-cscvs/devel;revno=433
1783 difftacular lp:~launchpad/difftacular/trunk;revno=11
1784-loggerhead lp:~loggerhead-team/loggerhead/trunk-rich;revno=506
1785+loggerhead lp:~loggerhead-team/loggerhead/trunk-rich;revno=511
1786 pygettextpo lp:~launchpad-pqm/pygettextpo/trunk;revno=25

Subscribers

People subscribed via source and target branches

to status/vote changes: