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 (community) 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
diff --git a/lib/lp/archivepublisher/archivegpgsigningkey.py b/lib/lp/archivepublisher/archivegpgsigningkey.py
index a342332..20a4d79 100644
--- a/lib/lp/archivepublisher/archivegpgsigningkey.py
+++ b/lib/lp/archivepublisher/archivegpgsigningkey.py
@@ -37,17 +37,24 @@ from lp.archivepublisher.run_parts import (
37from lp.registry.interfaces.gpg import IGPGKeySet37from lp.registry.interfaces.gpg import IGPGKeySet
38from lp.services.config import config38from lp.services.config import config
39from lp.services.features import getFeatureFlag39from lp.services.features import getFeatureFlag
40from lp.services.gpg.interfaces import IGPGHandler40from lp.services.gpg.interfaces import (
41 IGPGHandler,
42 IPymeKey,
43 )
41from lp.services.osutils import remove_if_exists44from lp.services.osutils import remove_if_exists
42from lp.services.propertycache import (45from lp.services.propertycache import (
43 cachedproperty,46 cachedproperty,
44 get_property_cache,47 get_property_cache,
45 )48 )
46from lp.services.signing.enums import (49from lp.services.signing.enums import (
50 OpenPGPKeyAlgorithm,
47 SigningKeyType,51 SigningKeyType,
48 SigningMode,52 SigningMode,
49 )53 )
50from lp.services.signing.interfaces.signingkey import ISigningKeySet54from lp.services.signing.interfaces.signingkey import (
55 ISigningKey,
56 ISigningKeySet,
57 )
5158
5259
53@implementer(ISignableArchive)60@implementer(ISignableArchive)
@@ -72,7 +79,7 @@ class SignableArchive:
72 def can_sign(self):79 def can_sign(self):
73 """See `ISignableArchive`."""80 """See `ISignableArchive`."""
74 return (81 return (
75 self.archive.signing_key is not None or82 self.archive.signing_key_fingerprint is not None or
76 self._run_parts_dir is not None)83 self._run_parts_dir is not None)
7784
78 @cachedproperty85 @cachedproperty
@@ -237,9 +244,9 @@ class ArchiveGPGSigningKey(SignableArchive):
237 with open(export_path, 'wb') as export_file:244 with open(export_path, 'wb') as export_file:
238 export_file.write(key.export())245 export_file.write(key.export())
239246
240 def generateSigningKey(self, log=None):247 def generateSigningKey(self, log=None, async_keyserver=False):
241 """See `IArchiveGPGSigningKey`."""248 """See `IArchiveGPGSigningKey`."""
242 assert self.archive.signing_key is None, (249 assert self.archive.signing_key_fingerprint is None, (
243 "Cannot override signing_keys.")250 "Cannot override signing_keys.")
244251
245 # Always generate signing keys for the default PPA, even if it252 # Always generate signing keys for the default PPA, even if it
@@ -257,13 +264,26 @@ class ArchiveGPGSigningKey(SignableArchive):
257264
258 key_displayname = (265 key_displayname = (
259 "Launchpad PPA for %s" % self.archive.owner.displayname)266 "Launchpad PPA for %s" % self.archive.owner.displayname)
260 secret_key = getUtility(IGPGHandler).generateKey(267 if getFeatureFlag(PUBLISHER_GPG_USES_SIGNING_SERVICE):
261 key_displayname, logger=log)268 try:
262 self._setupSigningKey(secret_key)269 signing_key = getUtility(ISigningKeySet).generate(
270 SigningKeyType.OPENPGP, key_displayname,
271 openpgp_key_algorithm=OpenPGPKeyAlgorithm.RSA, length=4096)
272 except Exception as e:
273 if log is not None:
274 log.exception(
275 "Error generating signing key for %s: %s %s" %
276 (self.archive.reference, e.__class__.__name__, e))
277 raise
278 else:
279 signing_key = getUtility(IGPGHandler).generateKey(
280 key_displayname, logger=log)
281 return self._setupSigningKey(
282 signing_key, async_keyserver=async_keyserver)
263283
264 def setSigningKey(self, key_path, async_keyserver=False):284 def setSigningKey(self, key_path, async_keyserver=False):
265 """See `IArchiveGPGSigningKey`."""285 """See `IArchiveGPGSigningKey`."""
266 assert self.archive.signing_key is None, (286 assert self.archive.signing_key_fingerprint is None, (
267 "Cannot override signing_keys.")287 "Cannot override signing_keys.")
268 assert os.path.exists(key_path), (288 assert os.path.exists(key_path), (
269 "%s does not exist" % key_path)289 "%s does not exist" % key_path)
@@ -274,34 +294,46 @@ class ArchiveGPGSigningKey(SignableArchive):
274 return self._setupSigningKey(294 return self._setupSigningKey(
275 secret_key, async_keyserver=async_keyserver)295 secret_key, async_keyserver=async_keyserver)
276296
277 def _uploadPublicSigningKey(self, secret_key):297 def _uploadPublicSigningKey(self, signing_key):
278 """Upload the public half of a signing key to the keyserver."""298 """Upload the public half of a signing key to the keyserver."""
279 # The handler's security proxying doesn't protect anything useful299 # The handler's security proxying doesn't protect anything useful
280 # here, and when we're running in a thread we don't have an300 # here, and when we're running in a thread we don't have an
281 # interaction.301 # interaction.
282 gpghandler = removeSecurityProxy(getUtility(IGPGHandler))302 gpghandler = removeSecurityProxy(getUtility(IGPGHandler))
283 pub_key = gpghandler.retrieveKey(secret_key.fingerprint)303 if IPymeKey.providedBy(signing_key):
284 gpghandler.uploadPublicKey(pub_key.fingerprint)304 pub_key = gpghandler.retrieveKey(signing_key.fingerprint)
285 return pub_key305 gpghandler.uploadPublicKey(pub_key.fingerprint)
306 return pub_key
307 else:
308 assert ISigningKey.providedBy(signing_key)
309 gpghandler.submitKey(removeSecurityProxy(signing_key).public_key)
310 return signing_key
286311
287 def _storeSigningKey(self, pub_key):312 def _storeSigningKey(self, pub_key):
288 """Store signing key reference in the database."""313 """Store signing key reference in the database."""
289 key_owner = getUtility(ILaunchpadCelebrities).ppa_key_guard314 key_owner = getUtility(ILaunchpadCelebrities).ppa_key_guard
290 key, _ = getUtility(IGPGKeySet).activate(315 if IPymeKey.providedBy(pub_key):
291 key_owner, pub_key, pub_key.can_encrypt)316 key, _ = getUtility(IGPGKeySet).activate(
292 self.archive.signing_key_owner = key.owner317 key_owner, pub_key, pub_key.can_encrypt)
318 else:
319 assert ISigningKey.providedBy(pub_key)
320 key = pub_key
321 self.archive.signing_key_owner = key_owner
293 self.archive.signing_key_fingerprint = key.fingerprint322 self.archive.signing_key_fingerprint = key.fingerprint
294 del get_property_cache(self.archive).signing_key323 del get_property_cache(self.archive).signing_key
295324
296 def _setupSigningKey(self, secret_key, async_keyserver=False):325 def _setupSigningKey(self, signing_key, async_keyserver=False):
297 """Mandatory setup for signing keys.326 """Mandatory setup for signing keys.
298327
299 * Export the secret key into the protected disk location.328 * Export the secret key into the protected disk location (for
329 locally-generated keys).
300 * Upload public key to the keyserver.330 * Upload public key to the keyserver.
301 * Store the public GPGKey reference in the database and update331 * Store the public GPGKey reference in the database (for
302 the context archive.signing_key.332 locally-generated keys) and update the context
333 archive.signing_key.
303 """334 """
304 self.exportSecretKey(secret_key)335 if IPymeKey.providedBy(signing_key):
336 self.exportSecretKey(signing_key)
305 if async_keyserver:337 if async_keyserver:
306 # If we have an asynchronous keyserver running in the current338 # If we have an asynchronous keyserver running in the current
307 # thread using Twisted, then we need some contortions to ensure339 # thread using Twisted, then we need some contortions to ensure
@@ -310,10 +342,10 @@ class ArchiveGPGSigningKey(SignableArchive):
310 # Since that thread won't have a Zope interaction, we need to342 # Since that thread won't have a Zope interaction, we need to
311 # unwrap the security proxy for it.343 # unwrap the security proxy for it.
312 d = deferToThread(344 d = deferToThread(
313 self._uploadPublicSigningKey, removeSecurityProxy(secret_key))345 self._uploadPublicSigningKey, removeSecurityProxy(signing_key))
314 d.addCallback(ProxyFactory)346 d.addCallback(ProxyFactory)
315 d.addCallback(self._storeSigningKey)347 d.addCallback(self._storeSigningKey)
316 return d348 return d
317 else:349 else:
318 pub_key = self._uploadPublicSigningKey(secret_key)350 pub_key = self._uploadPublicSigningKey(signing_key)
319 self._storeSigningKey(pub_key)351 self._storeSigningKey(pub_key)
diff --git a/lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py b/lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py
index 87359e8..67366bd 100644
--- a/lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py
+++ b/lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py
@@ -14,8 +14,15 @@ from testtools.matchers import (
14 FileContains,14 FileContains,
15 StartsWith,15 StartsWith,
16 )16 )
17from testtools.twistedsupport import AsynchronousDeferredRunTest17from testtools.twistedsupport import (
18from twisted.internet import defer18 AsynchronousDeferredRunTest,
19 AsynchronousDeferredRunTestForBrokenTwisted,
20 )
21import treq
22from twisted.internet import (
23 defer,
24 reactor,
25 )
19from zope.component import getUtility26from zope.component import getUtility
2027
21from lp.archivepublisher.config import getPubConfig28from lp.archivepublisher.config import getPubConfig
@@ -26,18 +33,27 @@ from lp.archivepublisher.interfaces.archivegpgsigningkey import (
26 )33 )
27from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet34from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
28from lp.archivepublisher.tests.test_run_parts import RunPartsMixin35from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
36from lp.registry.interfaces.gpg import IGPGKeySet
29from lp.services.compat import mock37from lp.services.compat import mock
30from lp.services.features.testing import FeatureFixture38from lp.services.features.testing import FeatureFixture
39from lp.services.gpg.interfaces import IGPGHandler
40from lp.services.gpg.tests.test_gpghandler import FakeGenerateKey
31from lp.services.log.logger import BufferLogger41from lp.services.log.logger import BufferLogger
32from lp.services.osutils import write_file42from lp.services.osutils import write_file
33from lp.services.signing.enums import (43from lp.services.signing.enums import (
34 SigningKeyType,44 SigningKeyType,
35 SigningMode,45 SigningMode,
36 )46 )
47from lp.services.signing.interfaces.signingkey import ISigningKeySet
37from lp.services.signing.tests.helpers import SigningServiceClientFixture48from lp.services.signing.tests.helpers import SigningServiceClientFixture
49from lp.services.twistedsupport.testing import TReqFixture
50from lp.services.twistedsupport.treq import check_status
38from lp.soyuz.enums import ArchivePurpose51from lp.soyuz.enums import ArchivePurpose
39from lp.testing import TestCaseWithFactory52from lp.testing import TestCaseWithFactory
40from lp.testing.gpgkeys import gpgkeysdir53from lp.testing.gpgkeys import (
54 gpgkeysdir,
55 test_pubkey_from_email,
56 )
41from lp.testing.keyserver import InProcessKeyServerFixture57from lp.testing.keyserver import InProcessKeyServerFixture
42from lp.testing.layers import ZopelessDatabaseLayer58from lp.testing.layers import ZopelessDatabaseLayer
4359
@@ -271,3 +287,89 @@ class TestSignableArchiveWithRunParts(RunPartsMixin, TestCaseWithFactory):
271 FileContains(287 FileContains(
272 "detached signature of %s (%s, %s/%s)\n" %288 "detached signature of %s (%s, %s/%s)\n" %
273 (filename, self.archive_root, self.distro.name, self.suite)))289 (filename, self.archive_root, self.distro.name, self.suite)))
290
291
292class TestArchiveGPGSigningKey(TestCaseWithFactory):
293
294 layer = ZopelessDatabaseLayer
295 # treq.content doesn't close the connection before yielding control back
296 # to the test, so we need to spin the reactor at the end to finish
297 # things off.
298 run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory(
299 timeout=10000)
300
301 @defer.inlineCallbacks
302 def setUp(self):
303 super(TestArchiveGPGSigningKey, self).setUp()
304 self.temp_dir = self.makeTemporaryDirectory()
305 self.pushConfig("personalpackagearchive", root=self.temp_dir)
306 self.keyserver = self.useFixture(InProcessKeyServerFixture())
307 yield self.keyserver.start()
308
309 @defer.inlineCallbacks
310 def test_generateSigningKey_local(self):
311 # Generating a signing key locally using GPGHandler stores it in the
312 # database and pushes it to the keyserver.
313 self.useFixture(FakeGenerateKey("ppa-sample@canonical.com.sec"))
314 logger = BufferLogger()
315 # Use a display name that matches the pregenerated sample key.
316 owner = self.factory.makePerson(
317 displayname="Celso \xe1\xe9\xed\xf3\xfa Providelo")
318 archive = self.factory.makeArchive(owner=owner)
319 yield IArchiveGPGSigningKey(archive).generateSigningKey(
320 log=logger, async_keyserver=True)
321 # The key is stored in the database.
322 self.assertIsNotNone(archive.signing_key_owner)
323 self.assertIsNotNone(archive.signing_key_fingerprint)
324 # The key is stored as a GPGKey, not a SigningKey.
325 self.assertIsNotNone(
326 getUtility(IGPGKeySet).getByFingerprint(
327 archive.signing_key_fingerprint))
328 self.assertIsNone(
329 getUtility(ISigningKeySet).get(
330 SigningKeyType.OPENPGP, archive.signing_key_fingerprint))
331 # The key is uploaded to the keyserver.
332 client = self.useFixture(TReqFixture(reactor)).client
333 response = yield client.get(
334 getUtility(IGPGHandler).getURLForKeyInServer(
335 archive.signing_key_fingerprint, "get"))
336 yield check_status(response)
337 content = yield treq.content(response)
338 self.assertIn(b"-----BEGIN PGP PUBLIC KEY BLOCK-----\n", content)
339
340 @defer.inlineCallbacks
341 def test_generateSigningKey_signing_service(self):
342 # Generating a signing key on the signing service stores it in the
343 # database and pushes it to the keyserver.
344 self.useFixture(
345 FeatureFixture({PUBLISHER_GPG_USES_SIGNING_SERVICE: "on"}))
346 signing_service_client = self.useFixture(
347 SigningServiceClientFixture(self.factory))
348 signing_service_client.generate.side_effect = None
349 test_key = test_pubkey_from_email("ftpmaster@canonical.com")
350 signing_service_client.generate.return_value = {
351 "fingerprint": "33C0A61893A5DC5EB325B29E415A12CAC2F30234",
352 "public-key": test_key,
353 }
354 logger = BufferLogger()
355 archive = self.factory.makeArchive()
356 yield IArchiveGPGSigningKey(archive).generateSigningKey(
357 log=logger, async_keyserver=True)
358 # The key is stored in the database.
359 self.assertIsNotNone(archive.signing_key_owner)
360 self.assertIsNotNone(archive.signing_key_fingerprint)
361 # The key is stored as a SigningKey, not a GPGKey.
362 self.assertIsNone(
363 getUtility(IGPGKeySet).getByFingerprint(
364 archive.signing_key_fingerprint))
365 signing_key = getUtility(ISigningKeySet).get(
366 SigningKeyType.OPENPGP, archive.signing_key_fingerprint)
367 self.assertEqual(test_key, signing_key.public_key)
368 # The key is uploaded to the keyserver.
369 client = self.useFixture(TReqFixture(reactor)).client
370 response = yield client.get(
371 getUtility(IGPGHandler).getURLForKeyInServer(
372 archive.signing_key_fingerprint, "get"))
373 yield check_status(response)
374 content = yield treq.content(response)
375 self.assertIn(test_key, content)
diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py
index aae704a..da94d43 100644
--- a/lib/lp/blueprints/browser/sprint.py
+++ b/lib/lp/blueprints/browser/sprint.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Sprint views."""4"""Sprint views."""
@@ -27,8 +27,8 @@ from collections import defaultdict
27import csv27import csv
2828
29from lazr.restful.utils import smartquote29from lazr.restful.utils import smartquote
30import six
31import pytz30import pytz
31import six
32from zope.component import getUtility32from zope.component import getUtility
33from zope.formlib.widget import CustomWidgetFactory33from zope.formlib.widget import CustomWidgetFactory
34from zope.formlib.widgets import TextAreaWidget34from zope.formlib.widgets import TextAreaWidget
@@ -462,6 +462,7 @@ class SprintTopicSetView(HasSpecificationsView, LaunchpadView):
462 # only a single item was selected, but we want to deal with a462 # only a single item was selected, but we want to deal with a
463 # list for the general case, so convert it to a list463 # list for the general case, so convert it to a list
464 selected_specs = [selected_specs]464 selected_specs = [selected_specs]
465 selected_specs = [int(speclink) for speclink in selected_specs]
465466
466 if action == 'Accepted':467 if action == 'Accepted':
467 action_fn = self.context.acceptSpecificationLinks468 action_fn = self.context.acceptSpecificationLinks
diff --git a/lib/lp/blueprints/browser/tests/test_views.py b/lib/lp/blueprints/browser/tests/test_views.py
index 5b68b37..cbbe2e5 100644
--- a/lib/lp/blueprints/browser/tests/test_views.py
+++ b/lib/lp/blueprints/browser/tests/test_views.py
@@ -110,7 +110,8 @@ def test_suite():
110 for filename in filenames:110 for filename in filenames:
111 path = filename111 path = filename
112 one_test = LayeredDocFileSuite(112 one_test = LayeredDocFileSuite(
113 path, setUp=setUp, tearDown=tearDown,113 path,
114 setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
114 layer=DatabaseFunctionalLayer,115 layer=DatabaseFunctionalLayer,
115 stdout_logging_level=logging.WARNING)116 stdout_logging_level=logging.WARNING)
116 suite.addTest(one_test)117 suite.addTest(one_test)
diff --git a/lib/lp/blueprints/model/specification.py b/lib/lp/blueprints/model/specification.py
index 40cf6ab..4b37d10 100644
--- a/lib/lp/blueprints/model/specification.py
+++ b/lib/lp/blueprints/model/specification.py
@@ -23,14 +23,15 @@ from sqlobject import (
23 SQLRelatedJoin,23 SQLRelatedJoin,
24 StringCol,24 StringCol,
25 )25 )
26from storm.expr import (26from storm.locals import (
27 Count,27 Count,
28 Desc,28 Desc,
29 Join,29 Join,
30 Or,30 Or,
31 ReferenceSet,
31 SQL,32 SQL,
33 Store,
32 )34 )
33from storm.store import Store
34from zope.component import getUtility35from zope.component import getUtility
35from zope.event import notify36from zope.event import notify
36from zope.interface import implementer37from zope.interface import implementer
@@ -237,11 +238,13 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
237 joinColumn='specification', otherColumn='person',238 joinColumn='specification', otherColumn='person',
238 intermediateTable='SpecificationSubscription',239 intermediateTable='SpecificationSubscription',
239 orderBy=['display_name', 'name'])240 orderBy=['display_name', 'name'])
240 sprint_links = SQLMultipleJoin('SprintSpecification', orderBy='id',241 sprint_links = ReferenceSet(
241 joinColumn='specification')242 '<primary key>', 'SprintSpecification.specification_id',
242 sprints = SQLRelatedJoin('Sprint', orderBy='name',243 order_by='SprintSpecification.id')
243 joinColumn='specification', otherColumn='sprint',244 sprints = ReferenceSet(
244 intermediateTable='SprintSpecification')245 '<primary key>', 'SprintSpecification.specification_id',
246 'SprintSpecification.sprint_id', 'Sprint.id',
247 order_by='Sprint.name')
245 spec_dependency_links = SQLMultipleJoin('SpecificationDependency',248 spec_dependency_links = SQLMultipleJoin('SpecificationDependency',
246 joinColumn='specification', orderBy='id')249 joinColumn='specification', orderBy='id')
247250
@@ -827,13 +830,11 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
827830
828 def unlinkSprint(self, sprint):831 def unlinkSprint(self, sprint):
829 """See ISpecification."""832 """See ISpecification."""
830 from lp.blueprints.model.sprintspecification import (
831 SprintSpecification)
832 for sprint_link in self.sprint_links:833 for sprint_link in self.sprint_links:
833 # sprints have unique names834 # sprints have unique names
834 if sprint_link.sprint.name == sprint.name:835 if sprint_link.sprint.name == sprint.name:
835 SprintSpecification.delete(sprint_link.id)836 sprint_link.destroySelf()
836 return sprint_link837 return
837838
838 # dependencies839 # dependencies
839 def createDependency(self, specification):840 def createDependency(self, specification):
@@ -1060,8 +1061,8 @@ class SpecificationSet(HasSpecificationsMixin):
1060 def coming_sprints(self):1061 def coming_sprints(self):
1061 """See ISpecificationSet."""1062 """See ISpecificationSet."""
1062 from lp.blueprints.model.sprint import Sprint1063 from lp.blueprints.model.sprint import Sprint
1063 return Sprint.select("time_ends > 'NOW'", orderBy='time_starts',1064 rows = IStore(Sprint).find(Sprint, Sprint.time_ends > UTC_NOW)
1064 limit=5)1065 return rows.order_by(Sprint.time_starts).config(limit=5)
10651066
1066 def new(self, name, title, specurl, summary, definition_status,1067 def new(self, name, title, specurl, summary, definition_status,
1067 owner, target, approver=None, assignee=None, drafter=None,1068 owner, target, approver=None, assignee=None, drafter=None,
diff --git a/lib/lp/blueprints/model/sprint.py b/lib/lp/blueprints/model/sprint.py
index 2446437..ed6748a 100644
--- a/lib/lp/blueprints/model/sprint.py
+++ b/lib/lp/blueprints/model/sprint.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2017 Canonical Ltd. This software is licensed under the1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -8,17 +8,17 @@ __all__ = [
8 'HasSprintsMixin',8 'HasSprintsMixin',
9 ]9 ]
1010
1111import pytz
12from sqlobject import (
13 BoolCol,
14 ForeignKey,
15 StringCol,
16 )
17from storm.locals import (12from storm.locals import (
13 Bool,
14 DateTime,
18 Desc,15 Desc,
16 Int,
19 Join,17 Join,
20 Or,18 Or,
19 Reference,
21 Store,20 Store,
21 Unicode,
22 )22 )
23from zope.component import getUtility23from zope.component import getUtility
24from zope.interface import implementer24from zope.interface import implementer
@@ -38,7 +38,10 @@ from lp.blueprints.interfaces.sprint import (
38 ISprint,38 ISprint,
39 ISprintSet,39 ISprintSet,
40 )40 )
41from lp.blueprints.model.specification import HasSpecificationsMixin41from lp.blueprints.model.specification import (
42 HasSpecificationsMixin,
43 Specification,
44 )
42from lp.blueprints.model.specificationsearch import (45from lp.blueprints.model.specificationsearch import (
43 get_specification_active_product_filter,46 get_specification_active_product_filter,
44 get_specification_filters,47 get_specification_filters,
@@ -51,46 +54,66 @@ from lp.registry.interfaces.person import (
51 validate_public_person,54 validate_public_person,
52 )55 )
53from lp.registry.model.hasdrivers import HasDriversMixin56from lp.registry.model.hasdrivers import HasDriversMixin
54from lp.services.database.constants import DEFAULT57from lp.services.database.constants import (
55from lp.services.database.datetimecol import UtcDateTimeCol58 DEFAULT,
56from lp.services.database.sqlbase import (59 UTC_NOW,
57 flush_database_updates,
58 quote,
59 SQLBase,
60 )60 )
61from lp.services.database.interfaces import IStore
62from lp.services.database.sqlbase import flush_database_updates
63from lp.services.database.stormbase import StormBase
61from lp.services.propertycache import cachedproperty64from lp.services.propertycache import cachedproperty
6265
6366
64@implementer(ISprint, IHasLogo, IHasMugshot, IHasIcon)67@implementer(ISprint, IHasLogo, IHasMugshot, IHasIcon)
65class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):68class Sprint(StormBase, HasDriversMixin, HasSpecificationsMixin):
66 """See `ISprint`."""69 """See `ISprint`."""
6770
68 _defaultOrder = ['name']71 __storm_table__ = 'Sprint'
72 __storm_order__ = ['name']
6973
70 # db field names74 # db field names
71 owner = ForeignKey(75 id = Int(primary=True)
72 dbName='owner', foreignKey='Person',76 owner_id = Int(
73 storm_validator=validate_public_person, notNull=True)77 name='owner', validator=validate_public_person, allow_none=False)
74 name = StringCol(notNull=True, alternateID=True)78 owner = Reference(owner_id, 'Person.id')
75 title = StringCol(notNull=True)79 name = Unicode(allow_none=False)
76 summary = StringCol(notNull=True)80 title = Unicode(allow_none=False)
77 driver = ForeignKey(81 summary = Unicode(allow_none=False)
78 dbName='driver', foreignKey='Person',82 driver_id = Int(name='driver', validator=validate_public_person)
79 storm_validator=validate_public_person)83 driver = Reference(driver_id, 'Person.id')
80 home_page = StringCol(notNull=False, default=None)84 home_page = Unicode(allow_none=True, default=None)
81 homepage_content = StringCol(default=None)85 homepage_content = Unicode(default=None)
82 icon = ForeignKey(86 icon_id = Int(name='icon', default=None)
83 dbName='icon', foreignKey='LibraryFileAlias', default=None)87 icon = Reference(icon_id, 'LibraryFileAlias.id')
84 logo = ForeignKey(88 logo_id = Int(name='logo', default=None)
85 dbName='logo', foreignKey='LibraryFileAlias', default=None)89 logo = Reference(logo_id, 'LibraryFileAlias.id')
86 mugshot = ForeignKey(90 mugshot_id = Int(name='mugshot', default=None)
87 dbName='mugshot', foreignKey='LibraryFileAlias', default=None)91 mugshot = Reference(mugshot_id, 'LibraryFileAlias.id')
88 address = StringCol(notNull=False, default=None)92 address = Unicode(allow_none=True, default=None)
89 datecreated = UtcDateTimeCol(notNull=True, default=DEFAULT)93 datecreated = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
90 time_zone = StringCol(notNull=True)94 time_zone = Unicode(allow_none=False)
91 time_starts = UtcDateTimeCol(notNull=True)95 time_starts = DateTime(tzinfo=pytz.UTC, allow_none=False)
92 time_ends = UtcDateTimeCol(notNull=True)96 time_ends = DateTime(tzinfo=pytz.UTC, allow_none=False)
93 is_physical = BoolCol(notNull=True, default=True)97 is_physical = Bool(allow_none=False, default=True)
98
99 def __init__(self, owner, name, title, time_zone, time_starts, time_ends,
100 summary, address=None, driver=None, home_page=None,
101 mugshot=None, logo=None, icon=None, is_physical=True):
102 super(Sprint, self).__init__()
103 self.owner = owner
104 self.name = name
105 self.title = title
106 self.time_zone = time_zone
107 self.time_starts = time_starts
108 self.time_ends = time_ends
109 self.summary = summary
110 self.address = address
111 self.driver = driver
112 self.home_page = home_page
113 self.mugshot = mugshot
114 self.logo = logo
115 self.icon = icon
116 self.is_physical = is_physical
94117
95 # attributes118 # attributes
96119
@@ -128,7 +151,7 @@ class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):
128 tables.append(Join(151 tables.append(Join(
129 SprintSpecification,152 SprintSpecification,
130 SprintSpecification.specification == Specification.id))153 SprintSpecification.specification == Specification.id))
131 query.append(SprintSpecification.sprintID == self.id)154 query.append(SprintSpecification.sprint == self)
132155
133 if not filter:156 if not filter:
134 # filter could be None or [] then we decide the default157 # filter could be None or [] then we decide the default
@@ -209,7 +232,7 @@ class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):
209 context. Here we are a sprint that could cover many products and/or232 context. Here we are a sprint that could cover many products and/or
210 distros.233 distros.
211 """234 """
212 speclink = SprintSpecification.get(speclink_id)235 speclink = Store.of(self).get(SprintSpecification, speclink_id)
213 assert (speclink.sprint.id == self.id)236 assert (speclink.sprint.id == self.id)
214 return speclink237 return speclink
215238
@@ -303,15 +326,16 @@ class SprintSet:
303326
304 def __getitem__(self, name):327 def __getitem__(self, name):
305 """See `ISprintSet`."""328 """See `ISprintSet`."""
306 return Sprint.selectOneBy(name=name)329 return IStore(Sprint).find(Sprint, name=name).one()
307330
308 def __iter__(self):331 def __iter__(self):
309 """See `ISprintSet`."""332 """See `ISprintSet`."""
310 return iter(Sprint.select("time_ends > 'NOW'", orderBy='time_starts'))333 return iter(IStore(Sprint).find(
334 Sprint, Sprint.time_ends > UTC_NOW).order_by(Sprint.time_starts))
311335
312 @property336 @property
313 def all(self):337 def all(self):
314 return Sprint.select(orderBy='-time_starts')338 return IStore(Sprint).find(Sprint).order_by(Sprint.time_starts)
315339
316 def new(self, owner, name, title, time_zone, time_starts, time_ends,340 def new(self, owner, name, title, time_zone, time_starts, time_ends,
317 summary, address=None, driver=None, home_page=None,341 summary, address=None, driver=None, home_page=None,
@@ -329,48 +353,50 @@ class HasSprintsMixin:
329 implementing IHasSprints.353 implementing IHasSprints.
330 """354 """
331355
332 def _getBaseQueryAndClauseTablesForQueryingSprints(self):356 def _getBaseClausesForQueryingSprints(self):
333 """Return the base SQL query and the clauseTables to be used when357 """Return the base Storm clauses to be used when querying sprints
334 querying sprints related to this object.358 related to this object.
335359
336 Subclasses must overwrite this method if it doesn't suit them.360 Subclasses must overwrite this method if it doesn't suit them.
337 """361 """
338 query = """362 try:
339 Specification.%s = %s363 table = getattr(self, "__storm_table__")
340 AND Specification.id = SprintSpecification.specification364 except AttributeError:
341 AND SprintSpecification.sprint = Sprint.id365 # XXX cjwatson 2020-09-10: Remove this once all inheritors have
342 AND SprintSpecification.status = %s366 # been converted from SQLObject to Storm.
343 """ % (self._table, self.id,367 table = getattr(self, "_table")
344 quote(SprintSpecificationStatus.ACCEPTED))368 return [
345 return query, ['Specification', 'SprintSpecification']369 getattr(Specification, table.lower()) == self,
370 Specification.id == SprintSpecification.specification_id,
371 SprintSpecification.sprint == Sprint.id,
372 SprintSpecification.status == SprintSpecificationStatus.ACCEPTED,
373 ]
346374
347 def getSprints(self):375 def getSprints(self):
348 query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()376 clauses = self._getBaseClausesForQueryingSprints()
349 return Sprint.select(377 return IStore(Sprint).find(Sprint, *clauses).order_by(
350 query, clauseTables=tables, orderBy='-time_starts', distinct=True)378 Desc(Sprint.time_starts)).config(distinct=True)
351379
352 @cachedproperty380 @cachedproperty
353 def sprints(self):381 def sprints(self):
354 """See IHasSprints."""382 """See IHasSprints."""
355 return list(self.getSprints())383 return list(self.getSprints())
356384
357 def getComingSprings(self):385 def getComingSprints(self):
358 query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()386 clauses = self._getBaseClausesForQueryingSprints()
359 query += " AND Sprint.time_ends > 'NOW'"387 clauses.append(Sprint.time_ends > UTC_NOW)
360 return Sprint.select(388 return IStore(Sprint).find(Sprint, *clauses).order_by(
361 query, clauseTables=tables, orderBy='time_starts',389 Sprint.time_starts).config(distinct=True, limit=5)
362 distinct=True, limit=5)
363390
364 @cachedproperty391 @cachedproperty
365 def coming_sprints(self):392 def coming_sprints(self):
366 """See IHasSprints."""393 """See IHasSprints."""
367 return list(self.getComingSprings())394 return list(self.getComingSprints())
368395
369 @property396 @property
370 def past_sprints(self):397 def past_sprints(self):
371 """See IHasSprints."""398 """See IHasSprints."""
372 query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()399 clauses = self._getBaseClausesForQueryingSprints()
373 query += " AND Sprint.time_ends <= 'NOW'"400 clauses.append(Sprint.time_ends <= UTC_NOW)
374 return Sprint.select(401 return IStore(Sprint).find(Sprint, *clauses).order_by(
375 query, clauseTables=tables, orderBy='-time_starts',402 Desc(Sprint.time_starts)).config(distinct=True)
376 distinct=True)
diff --git a/lib/lp/blueprints/model/sprintspecification.py b/lib/lp/blueprints/model/sprintspecification.py
index 46e691a..eed7649 100644
--- a/lib/lp/blueprints/model/sprintspecification.py
+++ b/lib/lp/blueprints/model/sprintspecification.py
@@ -1,13 +1,17 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
55
6__all__ = ['SprintSpecification']6__all__ = ['SprintSpecification']
77
8from sqlobject import (8import pytz
9 ForeignKey,9from storm.locals import (
10 StringCol,10 DateTime,
11 Int,
12 Reference,
13 Store,
14 Unicode,
11 )15 )
12from zope.interface import implementer16from zope.interface import implementer
1317
@@ -18,32 +22,41 @@ from lp.services.database.constants import (
18 DEFAULT,22 DEFAULT,
19 UTC_NOW,23 UTC_NOW,
20 )24 )
21from lp.services.database.datetimecol import UtcDateTimeCol25from lp.services.database.enumcol import DBEnum
22from lp.services.database.enumcol import EnumCol26from lp.services.database.stormbase import StormBase
23from lp.services.database.sqlbase import SQLBase
2427
2528
26@implementer(ISprintSpecification)29@implementer(ISprintSpecification)
27class SprintSpecification(SQLBase):30class SprintSpecification(StormBase):
28 """A link between a sprint and a specification."""31 """A link between a sprint and a specification."""
2932
30 _table = 'SprintSpecification'33 __storm_table__ = 'SprintSpecification'
3134
32 sprint = ForeignKey(dbName='sprint', foreignKey='Sprint',35 id = Int(primary=True)
33 notNull=True)36
34 specification = ForeignKey(dbName='specification',37 sprint_id = Int(name='sprint', allow_none=False)
35 foreignKey='Specification', notNull=True)38 sprint = Reference(sprint_id, 'Sprint.id')
36 status = EnumCol(schema=SprintSpecificationStatus, notNull=True,39 specification_id = Int(name='specification', allow_none=False)
40 specification = Reference(specification_id, 'Specification.id')
41 status = DBEnum(
42 enum=SprintSpecificationStatus, allow_none=False,
37 default=SprintSpecificationStatus.PROPOSED)43 default=SprintSpecificationStatus.PROPOSED)
38 whiteboard = StringCol(notNull=False, default=None)44 whiteboard = Unicode(allow_none=True, default=None)
39 registrant = ForeignKey(45 registrant_id = Int(
40 dbName='registrant', foreignKey='Person',46 name='registrant', validator=validate_public_person, allow_none=False)
41 storm_validator=validate_public_person, notNull=True)47 registrant = Reference(registrant_id, 'Person.id')
42 date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)48 date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
43 decider = ForeignKey(49 decider_id = Int(
44 dbName='decider', foreignKey='Person',50 name='decider', validator=validate_public_person, allow_none=True,
45 storm_validator=validate_public_person, notNull=False, default=None)51 default=None)
46 date_decided = UtcDateTimeCol(notNull=False, default=None)52 decider = Reference(decider_id, 'Person.id')
53 date_decided = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
54
55 def __init__(self, sprint, specification, registrant):
56 super(SprintSpecification, self).__init__()
57 self.sprint = sprint
58 self.specification = specification
59 self.registrant = registrant
4760
48 @property61 @property
49 def is_confirmed(self):62 def is_confirmed(self):
@@ -66,3 +79,6 @@ class SprintSpecification(SQLBase):
66 self.status = SprintSpecificationStatus.DECLINED79 self.status = SprintSpecificationStatus.DECLINED
67 self.decider = decider80 self.decider = decider
68 self.date_decided = UTC_NOW81 self.date_decided = UTC_NOW
82
83 def destroySelf(self):
84 Store.of(self).remove(self)
diff --git a/lib/lp/blueprints/vocabularies/sprint.py b/lib/lp/blueprints/vocabularies/sprint.py
index f300b62..f98df43 100644
--- a/lib/lp/blueprints/vocabularies/sprint.py
+++ b/lib/lp/blueprints/vocabularies/sprint.py
@@ -9,21 +9,17 @@ __all__ = [
9 'SprintVocabulary',9 'SprintVocabulary',
10 ]10 ]
1111
12
13from lp.blueprints.model.sprint import Sprint12from lp.blueprints.model.sprint import Sprint
14from lp.services.webapp.vocabulary import NamedSQLObjectVocabulary13from lp.services.database.constants import UTC_NOW
14from lp.services.webapp.vocabulary import NamedStormVocabulary
1515
1616
17class FutureSprintVocabulary(NamedSQLObjectVocabulary):17class FutureSprintVocabulary(NamedStormVocabulary):
18 """A vocab of all sprints that have not yet finished."""18 """A vocab of all sprints that have not yet finished."""
1919
20 _table = Sprint20 _table = Sprint
2121 _clauses = [Sprint.time_ends > UTC_NOW]
22 def __iter__(self):
23 future_sprints = Sprint.select("time_ends > 'NOW'")
24 for sprint in future_sprints:
25 yield(self.toTerm(sprint))
2622
2723
28class SprintVocabulary(NamedSQLObjectVocabulary):24class SprintVocabulary(NamedStormVocabulary):
29 _table = Sprint25 _table = Sprint
diff --git a/lib/lp/code/mail/tests/test_codehandler.py b/lib/lp/code/mail/tests/test_codehandler.py
index a02cc4d..97feda0 100644
--- a/lib/lp/code/mail/tests/test_codehandler.py
+++ b/lib/lp/code/mail/tests/test_codehandler.py
@@ -3,6 +3,8 @@
33
4"""Testing the CodeHandler."""4"""Testing the CodeHandler."""
55
6from __future__ import absolute_import, print_function, unicode_literals
7
6__metaclass__ = type8__metaclass__ = type
79
8from textwrap import dedent10from textwrap import dedent
diff --git a/lib/lp/code/mail/tests/test_codereviewcomment.py b/lib/lp/code/mail/tests/test_codereviewcomment.py
index 930753e..a692a97 100644
--- a/lib/lp/code/mail/tests/test_codereviewcomment.py
+++ b/lib/lp/code/mail/tests/test_codereviewcomment.py
@@ -243,7 +243,7 @@ class TestCodeReviewComment(TestCaseWithFactory):
243 def test_generateEmailWithVoteAndTag(self):243 def test_generateEmailWithVoteAndTag(self):
244 """Ensure that vote tags are displayed."""244 """Ensure that vote tags are displayed."""
245 mailer, subscriber = self.makeMailer(245 mailer, subscriber = self.makeMailer(
246 vote=CodeReviewVote.APPROVE, vote_tag='DBTAG')246 vote=CodeReviewVote.APPROVE, vote_tag=u'DBTAG')
247 ctrl = mailer.generateEmail(247 ctrl = mailer.generateEmail(
248 subscriber.preferredemail.email, subscriber)248 subscriber.preferredemail.email, subscriber)
249 self.assertEqual('Review: Approve dbtag', ctrl.body.splitlines()[0])249 self.assertEqual('Review: Approve dbtag', ctrl.body.splitlines()[0])
diff --git a/lib/lp/code/model/branchcollection.py b/lib/lp/code/model/branchcollection.py
index 7482778..bc3a026 100644
--- a/lib/lp/code/model/branchcollection.py
+++ b/lib/lp/code/model/branchcollection.py
@@ -484,10 +484,10 @@ class GenericBranchCollection:
484 tables = [484 tables = [
485 BranchMergeProposal,485 BranchMergeProposal,
486 Join(CodeReviewVoteReference,486 Join(CodeReviewVoteReference,
487 CodeReviewVoteReference.branch_merge_proposalID == \487 CodeReviewVoteReference.branch_merge_proposal ==
488 BranchMergeProposal.id),488 BranchMergeProposal.id),
489 LeftJoin(CodeReviewComment,489 LeftJoin(CodeReviewComment,
490 CodeReviewVoteReference.commentID == CodeReviewComment.id)]490 CodeReviewVoteReference.comment == CodeReviewComment.id)]
491491
492 expressions = [492 expressions = [
493 CodeReviewVoteReference.reviewer == reviewer,493 CodeReviewVoteReference.reviewer == reviewer,
diff --git a/lib/lp/code/model/branchmergeproposal.py b/lib/lp/code/model/branchmergeproposal.py
index 338cc29..2a346b4 100644
--- a/lib/lp/code/model/branchmergeproposal.py
+++ b/lib/lp/code/model/branchmergeproposal.py
@@ -46,6 +46,7 @@ from zope.interface import implementer
46from zope.security.interfaces import Unauthorized46from zope.security.interfaces import Unauthorized
4747
48from lp.app.enums import PRIVATE_INFORMATION_TYPES48from lp.app.enums import PRIVATE_INFORMATION_TYPES
49from lp.app.errors import NotFoundError
49from lp.app.interfaces.launchpad import ILaunchpadCelebrities50from lp.app.interfaces.launchpad import ILaunchpadCelebrities
50from lp.bugs.interfaces.bugtask import IBugTaskSet51from lp.bugs.interfaces.bugtask import IBugTaskSet
51from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context52from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context
@@ -565,11 +566,14 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
565 @property566 @property
566 def all_comments(self):567 def all_comments(self):
567 """See `IBranchMergeProposal`."""568 """See `IBranchMergeProposal`."""
568 return CodeReviewComment.selectBy(branch_merge_proposal=self.id)569 return IStore(CodeReviewComment).find(
570 CodeReviewComment, branch_merge_proposal=self)
569571
570 def getComment(self, id):572 def getComment(self, id):
571 """See `IBranchMergeProposal`."""573 """See `IBranchMergeProposal`."""
572 comment = CodeReviewComment.get(id)574 comment = IStore(CodeReviewComment).get(CodeReviewComment, id)
575 if comment is None:
576 raise NotFoundError(id)
573 if comment.branch_merge_proposal != self:577 if comment.branch_merge_proposal != self:
574 raise WrongBranchMergeProposal578 raise WrongBranchMergeProposal
575 return comment579 return comment
@@ -583,7 +587,10 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
583587
584 def setCommentVisibility(self, user, comment_number, visible):588 def setCommentVisibility(self, user, comment_number, visible):
585 """See `IBranchMergeProposal`."""589 """See `IBranchMergeProposal`."""
586 comment = CodeReviewComment.get(comment_number)590 comment = IStore(CodeReviewComment).get(
591 CodeReviewComment, comment_number)
592 if comment is None:
593 raise NotFoundError(comment_number)
587 if comment.branch_merge_proposal != self:594 if comment.branch_merge_proposal != self:
588 raise WrongBranchMergeProposal595 raise WrongBranchMergeProposal
589 if not comment.userCanSetCommentVisibility(user):596 if not comment.userCanSetCommentVisibility(user):
@@ -596,7 +603,9 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
596 """See `IBranchMergeProposal`.603 """See `IBranchMergeProposal`.
597604
598 This function can raise WrongBranchMergeProposal."""605 This function can raise WrongBranchMergeProposal."""
599 vote = CodeReviewVoteReference.get(id)606 vote = IStore(CodeReviewVoteReference).get(CodeReviewVoteReference, id)
607 if vote is None:
608 raise NotFoundError(id)
600 if vote.branch_merge_proposal != self:609 if vote.branch_merge_proposal != self:
601 raise WrongBranchMergeProposal610 raise WrongBranchMergeProposal
602 return vote611 return vote
@@ -932,6 +941,7 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
932 date_created=_date_created)941 date_created=_date_created)
933 self._ensureAssociatedBranchesVisibleToReviewer(reviewer)942 self._ensureAssociatedBranchesVisibleToReviewer(reviewer)
934 vote_reference.review_type = review_type943 vote_reference.review_type = review_type
944 Store.of(vote_reference).flush()
935 if _notify_listeners:945 if _notify_listeners:
936 notify(ReviewerNominatedEvent(vote_reference))946 notify(ReviewerNominatedEvent(vote_reference))
937 return vote_reference947 return vote_reference
@@ -1098,11 +1108,13 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
1098 if team_ref is not None:1108 if team_ref is not None:
1099 return team_ref1109 return team_ref
1100 # Create a new reference.1110 # Create a new reference.
1101 return CodeReviewVoteReference(1111 vote_reference = CodeReviewVoteReference(
1102 branch_merge_proposal=self,1112 branch_merge_proposal=self,
1103 registrant=user,1113 registrant=user,
1104 reviewer=user,1114 reviewer=user,
1105 review_type=review_type)1115 review_type=review_type)
1116 Store.of(vote_reference).flush()
1117 return vote_reference
11061118
1107 def createCommentFromMessage(self, message, vote, review_type,1119 def createCommentFromMessage(self, message, vote, review_type,
1108 original_email, _notify_listeners=True,1120 original_email, _notify_listeners=True,
@@ -1126,6 +1138,7 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
1126 vote_reference.reviewer = message.owner1138 vote_reference.reviewer = message.owner
1127 vote_reference.review_type = review_type1139 vote_reference.review_type = review_type
1128 vote_reference.comment = code_review_message1140 vote_reference.comment = code_review_message
1141 Store.of(code_review_message).flush()
1129 if _notify_listeners:1142 if _notify_listeners:
1130 notify(ObjectCreatedEvent(code_review_message))1143 notify(ObjectCreatedEvent(code_review_message))
1131 return code_review_message1144 return code_review_message
@@ -1389,15 +1402,15 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
1389 if include_votes:1402 if include_votes:
1390 votes = load_referencing(1403 votes = load_referencing(
1391 CodeReviewVoteReference, branch_merge_proposals,1404 CodeReviewVoteReference, branch_merge_proposals,
1392 ['branch_merge_proposalID'])1405 ['branch_merge_proposal_id'])
1393 votes_map = defaultdict(list)1406 votes_map = defaultdict(list)
1394 for vote in votes:1407 for vote in votes:
1395 votes_map[vote.branch_merge_proposalID].append(vote)1408 votes_map[vote.branch_merge_proposal_id].append(vote)
1396 for mp in branch_merge_proposals:1409 for mp in branch_merge_proposals:
1397 get_property_cache(mp).votes = votes_map[mp.id]1410 get_property_cache(mp).votes = votes_map[mp.id]
1398 comments = load_related(CodeReviewComment, votes, ['commentID'])1411 comments = load_related(CodeReviewComment, votes, ['comment_id'])
1399 load_related(Message, comments, ['messageID'])1412 load_related(Message, comments, ['message_id'])
1400 person_ids.update(vote.reviewerID for vote in votes)1413 person_ids.update(vote.reviewer_id for vote in votes)
14011414
1402 # we also provide a summary of diffs, so load them1415 # we also provide a summary of diffs, so load them
1403 load_related(LibraryFileAlias, diffs, ['diff_textID'])1416 load_related(LibraryFileAlias, diffs, ['diff_textID'])
@@ -1439,8 +1452,8 @@ class BranchMergeProposalGetter:
1439 BranchMergeProposal.registrantID == participant.id)1452 BranchMergeProposal.registrantID == participant.id)
14401453
1441 review_select = Select(1454 review_select = Select(
1442 [CodeReviewVoteReference.branch_merge_proposalID],1455 [CodeReviewVoteReference.branch_merge_proposal_id],
1443 [CodeReviewVoteReference.reviewerID == participant.id])1456 [CodeReviewVoteReference.reviewer == participant])
14441457
1445 query = Store.of(participant).find(1458 query = Store.of(participant).find(
1446 BranchMergeProposal,1459 BranchMergeProposal,
@@ -1463,13 +1476,13 @@ class BranchMergeProposalGetter:
1463 # the actual vote for that person.1476 # the actual vote for that person.
1464 tables = [1477 tables = [
1465 CodeReviewVoteReference,1478 CodeReviewVoteReference,
1466 Join(Person, CodeReviewVoteReference.reviewerID == Person.id),1479 Join(Person, CodeReviewVoteReference.reviewer == Person.id),
1467 LeftJoin(1480 LeftJoin(
1468 CodeReviewComment,1481 CodeReviewComment,
1469 CodeReviewVoteReference.commentID == CodeReviewComment.id)]1482 CodeReviewVoteReference.comment == CodeReviewComment.id)]
1470 results = store.using(*tables).find(1483 results = store.using(*tables).find(
1471 (CodeReviewVoteReference, Person, CodeReviewComment),1484 (CodeReviewVoteReference, Person, CodeReviewComment),
1472 CodeReviewVoteReference.branch_merge_proposalID.is_in(ids))1485 CodeReviewVoteReference.branch_merge_proposal_id.is_in(ids))
1473 for reference, person, comment in results:1486 for reference, person, comment in results:
1474 result[reference.branch_merge_proposal].append(reference)1487 result[reference.branch_merge_proposal].append(reference)
1475 return result1488 return result
diff --git a/lib/lp/code/model/codereviewcomment.py b/lib/lp/code/model/codereviewcomment.py
index 47f640c..1029b4e 100644
--- a/lib/lp/code/model/codereviewcomment.py
+++ b/lib/lp/code/model/codereviewcomment.py
@@ -10,9 +10,11 @@ __all__ = [
1010
11from textwrap import TextWrapper11from textwrap import TextWrapper
1212
13from sqlobject import (13from storm.locals import (
14 ForeignKey,14 Int,
15 StringCol,15 Reference,
16 Store,
17 Unicode,
16 )18 )
17from zope.interface import implementer19from zope.interface import implementer
1820
@@ -22,8 +24,8 @@ from lp.code.interfaces.codereviewcomment import (
22 ICodeReviewComment,24 ICodeReviewComment,
23 ICodeReviewCommentDeletion,25 ICodeReviewCommentDeletion,
24 )26 )
25from lp.services.database.enumcol import EnumCol27from lp.services.database.enumcol import DBEnum
26from lp.services.database.sqlbase import SQLBase28from lp.services.database.stormbase import StormBase
27from lp.services.mail.signedmessage import signed_message_from_string29from lp.services.mail.signedmessage import signed_message_from_string
2830
2931
@@ -60,17 +62,27 @@ def quote_text_as_email(text, width=80):
6062
6163
62@implementer(ICodeReviewComment, ICodeReviewCommentDeletion, IHasBranchTarget)64@implementer(ICodeReviewComment, ICodeReviewCommentDeletion, IHasBranchTarget)
63class CodeReviewComment(SQLBase):65class CodeReviewComment(StormBase):
64 """A table linking branch merge proposals and messages."""66 """A table linking branch merge proposals and messages."""
6567
66 _table = 'CodeReviewMessage'68 __storm_table__ = 'CodeReviewMessage'
6769
68 branch_merge_proposal = ForeignKey(70 id = Int(primary=True)
69 dbName='branch_merge_proposal', foreignKey='BranchMergeProposal',71 branch_merge_proposal_id = Int(
70 notNull=True)72 name='branch_merge_proposal', allow_none=False)
71 message = ForeignKey(dbName='message', foreignKey='Message', notNull=True)73 branch_merge_proposal = Reference(
72 vote = EnumCol(dbName='vote', notNull=False, schema=CodeReviewVote)74 branch_merge_proposal_id, 'BranchMergeProposal.id')
73 vote_tag = StringCol(default=None)75 message_id = Int(name='message', allow_none=False)
76 message = Reference(message_id, 'Message.id')
77 vote = DBEnum(name='vote', allow_none=True, enum=CodeReviewVote)
78 vote_tag = Unicode(default=None)
79
80 def __init__(self, branch_merge_proposal, message, vote=None,
81 vote_tag=None):
82 self.branch_merge_proposal = branch_merge_proposal
83 self.message = message
84 self.vote = vote
85 self.vote_tag = vote_tag
7486
75 @property87 @property
76 def author(self):88 def author(self):
@@ -134,3 +146,7 @@ class CodeReviewComment(SQLBase):
134 return (146 return (
135 self.branch_merge_proposal.userCanSetCommentVisibility(user) or147 self.branch_merge_proposal.userCanSetCommentVisibility(user) or
136 (user is not None and user.inTeam(self.author)))148 (user is not None and user.inTeam(self.author)))
149
150 def destroySelf(self):
151 """Delete this comment."""
152 Store.of(self).remove(self)
diff --git a/lib/lp/code/model/codereviewvote.py b/lib/lp/code/model/codereviewvote.py
index d2e5c53..b695ebe 100644
--- a/lib/lp/code/model/codereviewvote.py
+++ b/lib/lp/code/model/codereviewvote.py
@@ -8,12 +8,15 @@ __all__ = [
8 'CodeReviewVoteReference',8 'CodeReviewVoteReference',
9 ]9 ]
1010
11from sqlobject import (11import pytz
12 ForeignKey,12from storm.locals import (
13 StringCol,13 DateTime,
14 Int,
15 Reference,
16 Store,
17 Unicode,
14 )18 )
15from zope.interface import implementer19from zope.interface import implementer
16from zope.schema import Int
1720
18from lp.code.errors import (21from lp.code.errors import (
19 ClaimReviewFailed,22 ClaimReviewFailed,
@@ -22,27 +25,36 @@ from lp.code.errors import (
22 )25 )
23from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference26from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
24from lp.services.database.constants import DEFAULT27from lp.services.database.constants import DEFAULT
25from lp.services.database.datetimecol import UtcDateTimeCol28from lp.services.database.stormbase import StormBase
26from lp.services.database.sqlbase import SQLBase
2729
2830
29@implementer(ICodeReviewVoteReference)31@implementer(ICodeReviewVoteReference)
30class CodeReviewVoteReference(SQLBase):32class CodeReviewVoteReference(StormBase):
31 """See `ICodeReviewVote`"""33 """See `ICodeReviewVote`"""
3234
33 _table = 'CodeReviewVote'35 __storm_table__ = 'CodeReviewVote'
34 id = Int()36
35 branch_merge_proposal = ForeignKey(37 id = Int(primary=True)
36 dbName='branch_merge_proposal', foreignKey='BranchMergeProposal',38 branch_merge_proposal_id = Int(
37 notNull=True)39 name='branch_merge_proposal', allow_none=False)
38 date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)40 branch_merge_proposal = Reference(
39 registrant = ForeignKey(41 branch_merge_proposal_id, 'BranchMergeProposal.id')
40 dbName='registrant', foreignKey='Person', notNull=True)42 date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
41 reviewer = ForeignKey(43 registrant_id = Int(name='registrant', allow_none=False)
42 dbName='reviewer', foreignKey='Person', notNull=True)44 registrant = Reference(registrant_id, 'Person.id')
43 review_type = StringCol(default=None)45 reviewer_id = Int(name='reviewer', allow_none=False)
44 comment = ForeignKey(46 reviewer = Reference(reviewer_id, 'Person.id')
45 dbName='vote_message', foreignKey='CodeReviewComment', default=None)47 review_type = Unicode(default=None)
48 comment_id = Int(name='vote_message', default=None)
49 comment = Reference(comment_id, 'CodeReviewComment.id')
50
51 def __init__(self, branch_merge_proposal, registrant, reviewer,
52 review_type=None, date_created=DEFAULT):
53 self.branch_merge_proposal = branch_merge_proposal
54 self.registrant = registrant
55 self.reviewer = reviewer
56 self.review_type = review_type
57 self.date_created = date_created
4658
47 @property59 @property
48 def is_pending(self):60 def is_pending(self):
@@ -96,6 +108,10 @@ class CodeReviewVoteReference(SQLBase):
96 self.validateReasignReview(reviewer)108 self.validateReasignReview(reviewer)
97 self.reviewer = reviewer109 self.reviewer = reviewer
98110
111 def destroySelf(self):
112 """Delete this vote."""
113 Store.of(self).remove(self)
114
99 def delete(self):115 def delete(self):
100 """See `ICodeReviewVote`"""116 """See `ICodeReviewVote`"""
101 if not self.is_pending:117 if not self.is_pending:
diff --git a/lib/lp/code/model/gitcollection.py b/lib/lp/code/model/gitcollection.py
index 4095d5e..6447d0b 100644
--- a/lib/lp/code/model/gitcollection.py
+++ b/lib/lp/code/model/gitcollection.py
@@ -412,10 +412,10 @@ class GenericGitCollection:
412 tables = [412 tables = [
413 BranchMergeProposal,413 BranchMergeProposal,
414 Join(CodeReviewVoteReference,414 Join(CodeReviewVoteReference,
415 CodeReviewVoteReference.branch_merge_proposalID == \415 CodeReviewVoteReference.branch_merge_proposal ==
416 BranchMergeProposal.id),416 BranchMergeProposal.id),
417 LeftJoin(CodeReviewComment,417 LeftJoin(CodeReviewComment,
418 CodeReviewVoteReference.commentID == CodeReviewComment.id)]418 CodeReviewVoteReference.comment == CodeReviewComment.id)]
419419
420 expressions = [420 expressions = [
421 CodeReviewVoteReference.reviewer == reviewer,421 CodeReviewVoteReference.reviewer == reviewer,
diff --git a/lib/lp/code/model/tests/test_branch.py b/lib/lp/code/model/tests/test_branch.py
index 1978e88..e2421ba 100644
--- a/lib/lp/code/model/tests/test_branch.py
+++ b/lib/lp/code/model/tests/test_branch.py
@@ -1566,8 +1566,8 @@ class TestBranchDeletionConsequences(TestCase):
1566 comment_id = comment.id1566 comment_id = comment.id
1567 branch = comment.branch_merge_proposal.source_branch1567 branch = comment.branch_merge_proposal.source_branch
1568 branch.destroySelf(break_references=True)1568 branch.destroySelf(break_references=True)
1569 self.assertRaises(1569 self.assertIsNone(
1570 SQLObjectNotFound, CodeReviewComment.get, comment_id)1570 IStore(CodeReviewComment).get(CodeReviewComment, comment_id))
15711571
1572 def test_deleteTargetCodeReviewComment(self):1572 def test_deleteTargetCodeReviewComment(self):
1573 """Deletion of branches that have CodeReviewComments works."""1573 """Deletion of branches that have CodeReviewComments works."""
@@ -1575,8 +1575,8 @@ class TestBranchDeletionConsequences(TestCase):
1575 comment_id = comment.id1575 comment_id = comment.id
1576 branch = comment.branch_merge_proposal.target_branch1576 branch = comment.branch_merge_proposal.target_branch
1577 branch.destroySelf(break_references=True)1577 branch.destroySelf(break_references=True)
1578 self.assertRaises(1578 self.assertIsNone(
1579 SQLObjectNotFound, CodeReviewComment.get, comment_id)1579 IStore(CodeReviewComment).get(CodeReviewComment, comment_id))
15801580
1581 def test_branchWithBugRequirements(self):1581 def test_branchWithBugRequirements(self):
1582 """Deletion requirements for a branch with a bug are right."""1582 """Deletion requirements for a branch with a bug are right."""
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index a8b9c35..62bf865 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -1086,8 +1086,8 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
1086 comment_id = comment.id1086 comment_id = comment.id
1087 repository = comment.branch_merge_proposal.source_git_repository1087 repository = comment.branch_merge_proposal.source_git_repository
1088 repository.destroySelf(break_references=True)1088 repository.destroySelf(break_references=True)
1089 self.assertRaises(1089 self.assertIsNone(
1090 SQLObjectNotFound, CodeReviewComment.get, comment_id)1090 IStore(CodeReviewComment).get(CodeReviewComment, comment_id))
10911091
1092 def test_delete_target_CodeReviewComment(self):1092 def test_delete_target_CodeReviewComment(self):
1093 # Deletion of target repositories that have CodeReviewComments works.1093 # Deletion of target repositories that have CodeReviewComments works.
@@ -1095,8 +1095,8 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
1095 comment_id = comment.id1095 comment_id = comment.id
1096 repository = comment.branch_merge_proposal.target_git_repository1096 repository = comment.branch_merge_proposal.target_git_repository
1097 repository.destroySelf(break_references=True)1097 repository.destroySelf(break_references=True)
1098 self.assertRaises(1098 self.assertIsNone(
1099 SQLObjectNotFound, CodeReviewComment.get, comment_id)1099 IStore(CodeReviewComment).get(CodeReviewComment, comment_id))
11001100
1101 def test_sourceBranchWithCodeReviewVoteReference(self):1101 def test_sourceBranchWithCodeReviewVoteReference(self):
1102 # break_references handles CodeReviewVoteReference source repository.1102 # break_references handles CodeReviewVoteReference source repository.
diff --git a/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt b/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
index 1580ea4..98483a4 100644
--- a/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
+++ b/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
@@ -463,7 +463,7 @@ which is the one we want the method to return.
463 ... product=blob, set_state=BranchMergeProposalStatus.NEEDS_REVIEW,463 ... product=blob, set_state=BranchMergeProposalStatus.NEEDS_REVIEW,
464 ... registrant=branch_owner, source_branch=source_branch)464 ... registrant=branch_owner, source_branch=source_branch)
465 >>> proposal.nominateReviewer(target_owner, branch_owner)465 >>> proposal.nominateReviewer(target_owner, branch_owner)
466 <CodeReviewVoteReference at ...>466 <lp.code.model.codereviewvote.CodeReviewVoteReference object at ...>
467467
468And then we propose a merge the other way, so that the owner is target,468And then we propose a merge the other way, so that the owner is target,
469but they have not been asked to review, meaning that the method shouldn't469but they have not been asked to review, meaning that the method shouldn't
@@ -474,7 +474,7 @@ return this review.
474 ... product=blob, set_state=BranchMergeProposalStatus.NEEDS_REVIEW,474 ... product=blob, set_state=BranchMergeProposalStatus.NEEDS_REVIEW,
475 ... registrant=target_owner, source_branch=target_branch)475 ... registrant=target_owner, source_branch=target_branch)
476 >>> proposal.nominateReviewer(branch_owner, target_owner)476 >>> proposal.nominateReviewer(branch_owner, target_owner)
477 <CodeReviewVoteReference at ...>477 <lp.code.model.codereviewvote.CodeReviewVoteReference object at ...>
478 >>> logout()478 >>> logout()
479479
480 >>> proposals = webservice.named_get('/~target', 'getRequestedReviews'480 >>> proposals = webservice.named_get('/~target', 'getRequestedReviews'
diff --git a/lib/lp/codehosting/puller/tests/test_scheduler.py b/lib/lp/codehosting/puller/tests/test_scheduler.py
index 2b08509..34d53b6 100644
--- a/lib/lp/codehosting/puller/tests/test_scheduler.py
+++ b/lib/lp/codehosting/puller/tests/test_scheduler.py
@@ -553,7 +553,7 @@ class TestPullerMasterIntegration(PullerBranchTestCase):
553 """Tests for the puller master that launch sub-processes."""553 """Tests for the puller master that launch sub-processes."""
554554
555 layer = ZopelessAppServerLayer555 layer = ZopelessAppServerLayer
556 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)556 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
557557
558 def setUp(self):558 def setUp(self):
559 super(TestPullerMasterIntegration, self).setUp()559 super(TestPullerMasterIntegration, self).setUp()
diff --git a/lib/lp/registry/model/projectgroup.py b/lib/lp/registry/model/projectgroup.py
index 8a22fa8..bc6c5e5 100644
--- a/lib/lp/registry/model/projectgroup.py
+++ b/lib/lp/registry/model/projectgroup.py
@@ -50,7 +50,11 @@ from lp.blueprints.model.specification import (
50 Specification,50 Specification,
51 )51 )
52from lp.blueprints.model.specificationsearch import search_specifications52from lp.blueprints.model.specificationsearch import search_specifications
53from lp.blueprints.model.sprint import HasSprintsMixin53from lp.blueprints.model.sprint import (
54 HasSprintsMixin,
55 Sprint,
56 )
57from lp.blueprints.model.sprintspecification import SprintSpecification
54from lp.bugs.interfaces.bugsummary import IBugSummaryDimension58from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
55from lp.bugs.model.bugtarget import (59from lp.bugs.model.bugtarget import (
56 BugTargetBase,60 BugTargetBase,
@@ -239,15 +243,14 @@ class ProjectGroup(SQLBase, BugTargetBase, HasSpecificationsMixin,
239 """ See `IProjectGroup`."""243 """ See `IProjectGroup`."""
240 return not self.getBranches().is_empty()244 return not self.getBranches().is_empty()
241245
242 def _getBaseQueryAndClauseTablesForQueryingSprints(self):246 def _getBaseClausesForQueryingSprints(self):
243 query = """247 return [
244 Product.project = %s248 Product.projectgroup == self,
245 AND Specification.product = Product.id249 Specification.product == Product.id,
246 AND Specification.id = SprintSpecification.specification250 Specification.id == SprintSpecification.specification_id,
247 AND SprintSpecification.sprint = Sprint.id251 SprintSpecification.sprint == Sprint.id,
248 AND SprintSpecification.status = %s252 SprintSpecification.status == SprintSpecificationStatus.ACCEPTED,
249 """ % sqlvalues(self, SprintSpecificationStatus.ACCEPTED)253 ]
250 return query, ['Product', 'Specification', 'SprintSpecification']
251254
252 def specifications(self, user, sort=None, quantity=None, filter=None,255 def specifications(self, user, sort=None, quantity=None, filter=None,
253 series=None, need_people=True, need_branches=True,256 series=None, need_people=True, need_branches=True,
diff --git a/lib/lp/services/database/policy.py b/lib/lp/services/database/policy.py
index 066fdb3..691baf4 100644
--- a/lib/lp/services/database/policy.py
+++ b/lib/lp/services/database/policy.py
@@ -358,7 +358,7 @@ class LaunchpadDatabasePolicy(BaseDatabasePolicy):
358 slave_store = self.getStore(MAIN_STORE, SLAVE_FLAVOR)358 slave_store = self.getStore(MAIN_STORE, SLAVE_FLAVOR)
359 hot_standby, streaming_lag = slave_store.execute("""359 hot_standby, streaming_lag = slave_store.execute("""
360 SELECT360 SELECT
361 current_setting('hot_standby') = 'on',361 pg_is_in_recovery(),
362 now() - pg_last_xact_replay_timestamp()362 now() - pg_last_xact_replay_timestamp()
363 """).get_one()363 """).get_one()
364 if hot_standby and streaming_lag is not None:364 if hot_standby and streaming_lag is not None:
diff --git a/lib/lp/services/gpg/handler.py b/lib/lp/services/gpg/handler.py
index b2bcbad..e3eba45 100644
--- a/lib/lp/services/gpg/handler.py
+++ b/lib/lp/services/gpg/handler.py
@@ -489,12 +489,8 @@ class GPGHandler:
489 raise GPGKeyExpired(key)489 raise GPGKeyExpired(key)
490 return key490 return key
491491
492 def _submitKey(self, content):492 def submitKey(self, content):
493 """Submit an ASCII-armored public key export to the keyserver.493 """See `IGPGHandler`."""
494
495 It issues a POST at /pks/add on the keyserver specified in the
496 configuration.
497 """
498 keyserver_http_url = '%s:%s' % (494 keyserver_http_url = '%s:%s' % (
499 config.gpghandler.host, config.gpghandler.port)495 config.gpghandler.host, config.gpghandler.port)
500496
@@ -527,7 +523,7 @@ class GPGHandler:
527 return523 return
528524
529 pub_key = self.retrieveKey(fingerprint)525 pub_key = self.retrieveKey(fingerprint)
530 self._submitKey(pub_key.export())526 self.submitKey(pub_key.export())
531527
532 def getURLForKeyInServer(self, fingerprint, action='index', public=False):528 def getURLForKeyInServer(self, fingerprint, action='index', public=False):
533 """See IGPGHandler"""529 """See IGPGHandler"""
diff --git a/lib/lp/services/gpg/interfaces.py b/lib/lp/services/gpg/interfaces.py
index 78b44c8..d6f0f73 100644
--- a/lib/lp/services/gpg/interfaces.py
+++ b/lib/lp/services/gpg/interfaces.py
@@ -357,6 +357,17 @@ class IGPGHandler(Interface):
357 :return: a `PymeKey`object containing the key information.357 :return: a `PymeKey`object containing the key information.
358 """358 """
359359
360 def submitKey(content):
361 """Submit an ASCII-armored public key export to the keyserver.
362
363 It issues a POST at /pks/add on the keyserver specified in the
364 configuration.
365
366 :param content: The exported public key, as a byte string.
367 :raise GPGUploadFailure: if the keyserver could not be reached.
368 :raise AssertionError: if the POST request failed.
369 """
370
360 def uploadPublicKey(fingerprint):371 def uploadPublicKey(fingerprint):
361 """Upload the specified public key to a keyserver.372 """Upload the specified public key to a keyserver.
362373
@@ -365,8 +376,8 @@ class IGPGHandler(Interface):
365376
366 :param fingerprint: The key fingerprint, which must be an hexadecimal377 :param fingerprint: The key fingerprint, which must be an hexadecimal
367 string.378 string.
368 :raise GPGUploadFailure: if the keyserver could not be reaches.379 :raise GPGUploadFailure: if the keyserver could not be reached.
369 :raise AssertionError: if the POST request doesn't succeed.380 :raise AssertionError: if the POST request failed.
370 """381 """
371382
372 def localKeys(filter=None, secret=False):383 def localKeys(filter=None, secret=False):
diff --git a/lib/lp/services/librarianserver/tests/test_storage_db.py b/lib/lp/services/librarianserver/tests/test_storage_db.py
index 47fe847..b24d87b 100644
--- a/lib/lp/services/librarianserver/tests/test_storage_db.py
+++ b/lib/lp/services/librarianserver/tests/test_storage_db.py
@@ -146,7 +146,7 @@ class LibrarianStorageDBTests(TestCase):
146class LibrarianStorageSwiftTests(TestCase):146class LibrarianStorageSwiftTests(TestCase):
147147
148 layer = LaunchpadZopelessLayer148 layer = LaunchpadZopelessLayer
149 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)149 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
150150
151 def setUp(self):151 def setUp(self):
152 super(LibrarianStorageSwiftTests, self).setUp()152 super(LibrarianStorageSwiftTests, self).setUp()
diff --git a/lib/lp/services/mail/helpers.py b/lib/lp/services/mail/helpers.py
index 82640aa..80f68d3 100644
--- a/lib/lp/services/mail/helpers.py
+++ b/lib/lp/services/mail/helpers.py
@@ -35,7 +35,13 @@ class IncomingEmailError(Exception):
3535
3636
37def get_main_body(signed_msg):37def get_main_body(signed_msg):
38 """Returns the first text part of the email."""38 """Returns the first text part of the email.
39
40 This always returns text (or None if the email has no text parts at
41 all). It decodes using the character set in the text part's
42 Content-Type, or ISO-8859-1 if unspecified (in order to minimise the
43 chances of `UnicodeDecodeError`s).
44 """
39 msg = getattr(signed_msg, 'signedMessage', None)45 msg = getattr(signed_msg, 'signedMessage', None)
40 if msg is None:46 if msg is None:
41 # The email wasn't signed.47 # The email wasn't signed.
@@ -43,9 +49,11 @@ def get_main_body(signed_msg):
43 if msg.is_multipart():49 if msg.is_multipart():
44 for part in msg.walk():50 for part in msg.walk():
45 if part.get_content_type() == 'text/plain':51 if part.get_content_type() == 'text/plain':
46 return part.get_payload(decode=True)52 charset = part.get_content_charset('ISO-8859-1')
53 return part.get_payload(decode=True).decode(charset)
47 else:54 else:
48 return msg.get_payload(decode=True)55 charset = msg.get_content_charset('ISO-8859-1')
56 return msg.get_payload(decode=True).decode(charset)
4957
5058
51def guess_bugtask(bug, person):59def guess_bugtask(bug, person):
diff --git a/lib/lp/services/signing/tests/helpers.py b/lib/lp/services/signing/tests/helpers.py
index f819745..6831edb 100644
--- a/lib/lp/services/signing/tests/helpers.py
+++ b/lib/lp/services/signing/tests/helpers.py
@@ -49,7 +49,7 @@ class SigningServiceClientFixture(fixtures.Fixture):
49 openpgp_key_algorithm=None, length=None):49 openpgp_key_algorithm=None, length=None):
50 key = bytes(PrivateKey.generate().public_key)50 key = bytes(PrivateKey.generate().public_key)
51 data = {51 data = {
52 "fingerprint": self.factory.getUniqueHexString(40),52 "fingerprint": self.factory.getUniqueHexString(40).upper(),
53 "public-key": key,53 "public-key": key,
54 }54 }
55 self.generate_returns.append((key_type, data))55 self.generate_returns.append((key_type, data))
@@ -69,7 +69,7 @@ class SigningServiceClientFixture(fixtures.Fixture):
6969
70 def _inject(self, key_type, private_key, public_key, description,70 def _inject(self, key_type, private_key, public_key, description,
71 created_at):71 created_at):
72 data = {'fingerprint': self.factory.getUniqueHexString(40)}72 data = {'fingerprint': self.factory.getUniqueHexString(40).upper()}
73 self.inject_returns.append(data)73 self.inject_returns.append(data)
74 return data74 return data
7575
diff --git a/lib/lp/services/worlddata/vocabularies.py b/lib/lp/services/worlddata/vocabularies.py
index 58d2c8e..be963f4 100644
--- a/lib/lp/services/worlddata/vocabularies.py
+++ b/lib/lp/services/worlddata/vocabularies.py
@@ -1,6 +1,8 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4from __future__ import absolute_import, print_function, unicode_literals
5
4__all__ = [6__all__ = [
5 'CountryNameVocabulary',7 'CountryNameVocabulary',
6 'LanguageVocabulary',8 'LanguageVocabulary',
@@ -10,6 +12,7 @@ __all__ = [
10__metaclass__ = type12__metaclass__ = type
1113
12import pytz14import pytz
15import six
13from sqlobject import SQLObjectNotFound16from sqlobject import SQLObjectNotFound
14from zope.interface import alsoProvides17from zope.interface import alsoProvides
15from zope.schema.vocabulary import (18from zope.schema.vocabulary import (
@@ -24,7 +27,7 @@ from lp.services.worlddata.model.country import Country
24from lp.services.worlddata.model.language import Language27from lp.services.worlddata.model.language import Language
2528
26# create a sorted list of the common time zone names, with UTC at the start29# create a sorted list of the common time zone names, with UTC at the start
27_values = sorted(pytz.common_timezones)30_values = sorted(six.ensure_text(tz) for tz in pytz.common_timezones)
28_values.remove('UTC')31_values.remove('UTC')
29_values.insert(0, 'UTC')32_values.insert(0, 'UTC')
3033
diff --git a/lib/lp/soyuz/adapters/tests/test_archivedependencies.py b/lib/lp/soyuz/adapters/tests/test_archivedependencies.py
index 7cd3cad..d5a2068 100644
--- a/lib/lp/soyuz/adapters/tests/test_archivedependencies.py
+++ b/lib/lp/soyuz/adapters/tests/test_archivedependencies.py
@@ -128,7 +128,7 @@ class TestSourcesList(TestCaseWithFactory):
128 """Test sources.list contents for building, and related mechanisms."""128 """Test sources.list contents for building, and related mechanisms."""
129129
130 layer = LaunchpadZopelessLayer130 layer = LaunchpadZopelessLayer
131 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)131 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
132132
133 ubuntu_components = [133 ubuntu_components = [
134 "main", "restricted", "universe", "multiverse", "partner"]134 "main", "restricted", "universe", "multiverse", "partner"]
diff --git a/lib/lp/soyuz/configure.zcml b/lib/lp/soyuz/configure.zcml
index 2e97958..643a85b 100644
--- a/lib/lp/soyuz/configure.zcml
+++ b/lib/lp/soyuz/configure.zcml
@@ -373,7 +373,6 @@
373 set_schema="lp.soyuz.interfaces.archive.IArchiveRestricted"/>373 set_schema="lp.soyuz.interfaces.archive.IArchiveRestricted"/>
374 <require374 <require
375 permission="launchpad.InternalScriptsOnly"375 permission="launchpad.InternalScriptsOnly"
376 attributes="signing_key_owner"
377 set_attributes="dirty_suites distribution signing_key_owner376 set_attributes="dirty_suites distribution signing_key_owner
378 signing_key_fingerprint"/>377 signing_key_fingerprint"/>
379 </class>378 </class>
diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py
index 407f953..b6cc663 100644
--- a/lib/lp/soyuz/interfaces/archive.py
+++ b/lib/lp/soyuz/interfaces/archive.py
@@ -461,6 +461,8 @@ class IArchiveSubscriberView(Interface):
461 "explicit publish flag and any other constraints."))461 "explicit publish flag and any other constraints."))
462 series_with_sources = Attribute(462 series_with_sources = Attribute(
463 "DistroSeries to which this archive has published sources")463 "DistroSeries to which this archive has published sources")
464 signing_key_owner = Reference(
465 title=_("Archive signing key owner"), required=False, schema=IPerson)
464 signing_key_fingerprint = exported(466 signing_key_fingerprint = exported(
465 Text(467 Text(
466 title=_("Archive signing key fingerprint"), required=False,468 title=_("Archive signing key fingerprint"), required=False,
diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py
index a37b737..0d58e56 100644
--- a/lib/lp/soyuz/model/archive.py
+++ b/lib/lp/soyuz/model/archive.py
@@ -466,7 +466,7 @@ class Archive(SQLBase):
466 return (466 return (
467 not config.personalpackagearchive.require_signing_keys or467 not config.personalpackagearchive.require_signing_keys or
468 not self.is_ppa or468 not self.is_ppa or
469 self.signing_key is not None)469 self.signing_key_fingerprint is not None)
470470
471 @property471 @property
472 def reference(self):472 def reference(self):
@@ -2717,10 +2717,12 @@ class ArchiveSet:
2717 (owner.name, distribution.name, name))2717 (owner.name, distribution.name, name))
27182718
2719 # Signing-key for the default PPA is reused when it's already present.2719 # Signing-key for the default PPA is reused when it's already present.
2720 signing_key = None2720 signing_key_owner = None
2721 signing_key_fingerprint = None
2721 if purpose == ArchivePurpose.PPA:2722 if purpose == ArchivePurpose.PPA:
2722 if owner.archive is not None:2723 if owner.archive is not None:
2723 signing_key = owner.archive.signing_key2724 signing_key_owner = owner.archive.signing_key_owner
2725 signing_key_fingerprint = owner.archive.signing_key_fingerprint
2724 else:2726 else:
2725 # owner.archive is a cached property and we've just cached it.2727 # owner.archive is a cached property and we've just cached it.
2726 del get_property_cache(owner).archive2728 del get_property_cache(owner).archive
@@ -2729,9 +2731,8 @@ class ArchiveSet:
2729 owner=owner, distribution=distribution, name=name,2731 owner=owner, distribution=distribution, name=name,
2730 displayname=displayname, description=description,2732 displayname=displayname, description=description,
2731 purpose=purpose, publish=publish,2733 purpose=purpose, publish=publish,
2732 signing_key_owner=signing_key.owner if signing_key else None,2734 signing_key_owner=signing_key_owner,
2733 signing_key_fingerprint=(2735 signing_key_fingerprint=signing_key_fingerprint,
2734 signing_key.fingerprint if signing_key else None),
2735 require_virtualized=require_virtualized)2736 require_virtualized=require_virtualized)
27362737
2737 # Upon creation archives are enabled by default.2738 # Upon creation archives are enabled by default.
diff --git a/lib/lp/soyuz/scripts/ppakeygenerator.py b/lib/lp/soyuz/scripts/ppakeygenerator.py
index 190b4a0..88e1d84 100644
--- a/lib/lp/soyuz/scripts/ppakeygenerator.py
+++ b/lib/lp/soyuz/scripts/ppakeygenerator.py
@@ -34,7 +34,7 @@ class PPAKeyGenerator(LaunchpadCronScript):
34 (archive.reference, archive.displayname))34 (archive.reference, archive.displayname))
35 archive_signing_key = IArchiveGPGSigningKey(archive)35 archive_signing_key = IArchiveGPGSigningKey(archive)
36 archive_signing_key.generateSigningKey(log=self.logger)36 archive_signing_key.generateSigningKey(log=self.logger)
37 self.logger.info("Key %s" % archive.signing_key.fingerprint)37 self.logger.info("Key %s" % archive.signing_key_fingerprint)
3838
39 def main(self):39 def main(self):
40 """Generate signing keys for the selected PPAs."""40 """Generate signing keys for the selected PPAs."""
@@ -45,11 +45,11 @@ class PPAKeyGenerator(LaunchpadCronScript):
45 raise LaunchpadScriptFailure(45 raise LaunchpadScriptFailure(
46 "No archive named '%s' could be found."46 "No archive named '%s' could be found."
47 % self.options.archive)47 % self.options.archive)
48 if archive.signing_key is not None:48 if archive.signing_key_fingerprint is not None:
49 raise LaunchpadScriptFailure(49 raise LaunchpadScriptFailure(
50 "%s (%s) already has a signing_key (%s)"50 "%s (%s) already has a signing_key (%s)"
51 % (archive.reference, archive.displayname,51 % (archive.reference, archive.displayname,
52 archive.signing_key.fingerprint))52 archive.signing_key_fingerprint))
53 archives = [archive]53 archives = [archive]
54 else:54 else:
55 archive_set = getUtility(IArchiveSet)55 archive_set = getUtility(IArchiveSet)
diff --git a/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py b/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py
index 56e8710..a5d3caf 100644
--- a/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py
+++ b/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py
@@ -83,7 +83,7 @@ class TestPPAKeyGenerator(TestCase):
83 LaunchpadScriptFailure,83 LaunchpadScriptFailure,
84 ("~cprov/ubuntu/ppa (PPA for Celso Providelo) already has a "84 ("~cprov/ubuntu/ppa (PPA for Celso Providelo) already has a "
85 "signing_key (%s)" %85 "signing_key (%s)" %
86 cprov.archive.signing_key.fingerprint),86 cprov.archive.signing_key_fingerprint),
87 key_generator.main)87 key_generator.main)
8888
89 def testGenerateKeyForASinglePPA(self):89 def testGenerateKeyForASinglePPA(self):
@@ -95,14 +95,14 @@ class TestPPAKeyGenerator(TestCase):
95 cprov = getUtility(IPersonSet).getByName('cprov')95 cprov = getUtility(IPersonSet).getByName('cprov')
96 self._fixArchiveForKeyGeneration(cprov.archive)96 self._fixArchiveForKeyGeneration(cprov.archive)
9797
98 self.assertTrue(cprov.archive.signing_key is None)98 self.assertIsNone(cprov.archive.signing_key_fingerprint)
9999
100 txn = FakeTransaction()100 txn = FakeTransaction()
101 key_generator = self._getKeyGenerator(101 key_generator = self._getKeyGenerator(
102 archive_reference='~cprov/ubuntutest/ppa', txn=txn)102 archive_reference='~cprov/ubuntutest/ppa', txn=txn)
103 key_generator.main()103 key_generator.main()
104104
105 self.assertTrue(cprov.archive.signing_key is not None)105 self.assertIsNotNone(cprov.archive.signing_key_fingerprint)
106 self.assertEqual(txn.commit_count, 1)106 self.assertEqual(txn.commit_count, 1)
107107
108 def testGenerateKeyForAllPPA(self):108 def testGenerateKeyForAllPPA(self):
@@ -115,13 +115,13 @@ class TestPPAKeyGenerator(TestCase):
115115
116 for archive in archives:116 for archive in archives:
117 self._fixArchiveForKeyGeneration(archive)117 self._fixArchiveForKeyGeneration(archive)
118 self.assertTrue(archive.signing_key is None)118 self.assertIsNone(archive.signing_key_fingerprint)
119119
120 txn = FakeTransaction()120 txn = FakeTransaction()
121 key_generator = self._getKeyGenerator(txn=txn)121 key_generator = self._getKeyGenerator(txn=txn)
122 key_generator.main()122 key_generator.main()
123123
124 for archive in archives:124 for archive in archives:
125 self.assertTrue(archive.signing_key is not None)125 self.assertIsNotNone(archive.signing_key_fingerprint)
126126
127 self.assertEqual(txn.commit_count, len(archives))127 self.assertEqual(txn.commit_count, len(archives))
diff --git a/lib/lp/soyuz/stories/soyuz/xx-person-packages.txt b/lib/lp/soyuz/stories/soyuz/xx-person-packages.txt
index d815121..95ff407 100644
--- a/lib/lp/soyuz/stories/soyuz/xx-person-packages.txt
+++ b/lib/lp/soyuz/stories/soyuz/xx-person-packages.txt
@@ -400,7 +400,7 @@ Then delete the 'source2' package.
400 ... print(extract_text(empty_section))400 ... print(extract_text(empty_section))
401 >>> print_ppa_packages(admin_browser.contents)401 >>> print_ppa_packages(admin_browser.contents)
402 Source Published Status Series Section Build Status402 Source Published Status Series Section Build Status
403 source2 - 666... a moment ago Deleted ...403 source2 - 666... Deleted ...
404 >>> update_cached_records()404 >>> update_cached_records()
405405
406Now re-list the PPA's packages, 'source2' was deleted but still406Now re-list the PPA's packages, 'source2' was deleted but still
diff --git a/lib/lp/soyuz/tests/test_archive.py b/lib/lp/soyuz/tests/test_archive.py
index 91acf53..5c3f3b6 100644
--- a/lib/lp/soyuz/tests/test_archive.py
+++ b/lib/lp/soyuz/tests/test_archive.py
@@ -4046,7 +4046,7 @@ class TestSigningKeyPropagation(TestCaseWithFactory):
40464046
4047 def test_ppa_created_with_no_signing_key(self):4047 def test_ppa_created_with_no_signing_key(self):
4048 ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)4048 ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
4049 self.assertIsNone(ppa.signing_key)4049 self.assertIsNone(ppa.signing_key_fingerprint)
40504050
4051 def test_default_signing_key_propagated_to_new_ppa(self):4051 def test_default_signing_key_propagated_to_new_ppa(self):
4052 person = self.factory.makePerson()4052 person = self.factory.makePerson()
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index f5ad237..c109014 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -1121,14 +1121,14 @@ class BareLaunchpadObjectFactory(ObjectFactory):
1121 def makeSprint(self, title=None, name=None):1121 def makeSprint(self, title=None, name=None):
1122 """Make a sprint."""1122 """Make a sprint."""
1123 if title is None:1123 if title is None:
1124 title = self.getUniqueString('title')1124 title = self.getUniqueUnicode('title')
1125 owner = self.makePerson()1125 owner = self.makePerson()
1126 if name is None:1126 if name is None:
1127 name = self.getUniqueString('name')1127 name = self.getUniqueUnicode('name')
1128 time_starts = datetime(2009, 1, 1, tzinfo=pytz.UTC)1128 time_starts = datetime(2009, 1, 1, tzinfo=pytz.UTC)
1129 time_ends = datetime(2009, 1, 2, tzinfo=pytz.UTC)1129 time_ends = datetime(2009, 1, 2, tzinfo=pytz.UTC)
1130 time_zone = 'UTC'1130 time_zone = u'UTC'
1131 summary = self.getUniqueString('summary')1131 summary = self.getUniqueUnicode('summary')
1132 return getUtility(ISprintSet).new(1132 return getUtility(ISprintSet).new(
1133 owner=owner, name=name, title=title, time_zone=time_zone,1133 owner=owner, name=name, title=title, time_zone=time_zone,
1134 time_starts=time_starts, time_ends=time_ends, summary=summary)1134 time_starts=time_starts, time_ends=time_ends, summary=summary)
diff --git a/lib/lp/translations/pottery/tests/test_detect_intltool.py b/lib/lp/translations/pottery/tests/test_detect_intltool.py
index cc0d5d7..21dce33 100644
--- a/lib/lp/translations/pottery/tests/test_detect_intltool.py
+++ b/lib/lp/translations/pottery/tests/test_detect_intltool.py
@@ -6,9 +6,11 @@ __metaclass__ = type
6import errno6import errno
7import os7import os
8import tarfile8import tarfile
9from textwrap import dedent
910
10from breezy.controldir import ControlDir11from breezy.controldir import ControlDir
1112
13from lp.services.scripts.tests import run_script
12from lp.testing import TestCase14from lp.testing import TestCase
13from lp.translations.pottery.detect_intltool import is_intltool_structure15from lp.translations.pottery.detect_intltool import is_intltool_structure
1416
@@ -52,6 +54,18 @@ class SetupTestPackageMixin:
52 with open(path, 'w') as the_file:54 with open(path, 'w') as the_file:
53 the_file.write(content)55 the_file.write(content)
5456
57 def test_pottery_generate_intltool_script(self):
58 # Let the script run to see it works fine.
59 self.prepare_package("intltool_POTFILES_in_2")
60
61 return_code, stdout, stderr = run_script(
62 'scripts/rosetta/pottery-generate-intltool.py', [])
63
64 self.assertEqual(dedent("""\
65 module1/po/messages.pot
66 po/messages.pot
67 """), stdout)
68
5569
56class TestDetectIntltoolInBzrTree(TestCase, SetupTestPackageMixin):70class TestDetectIntltoolInBzrTree(TestCase, SetupTestPackageMixin):
5771
diff --git a/scripts/rosetta/pottery-generate-intltool.py b/scripts/rosetta/pottery-generate-intltool.py
58new file mode 10075572new file mode 100755
index 0000000..4557676
--- /dev/null
+++ b/scripts/rosetta/pottery-generate-intltool.py
@@ -0,0 +1,56 @@
1#!/usr/bin/python2 -S
2#
3# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6"""Print a list of directories that contain a valid intltool structure."""
7
8from __future__ import absolute_import, print_function, unicode_literals
9
10import _pythonpath
11
12import os.path
13
14from lpbuildd.pottery.intltool import generate_pots
15from lpbuildd.tests.fakeslave import UncontainedBackend as _UncontainedBackend
16
17from lp.services.scripts.base import LaunchpadScript
18
19
20class UncontainedBackend(_UncontainedBackend):
21 """Like UncontainedBackend, except avoid executing "test".
22
23 Otherwise we can end up with confusion between the Unix "test" utility
24 and Launchpad's bin/test.
25 """
26
27 def path_exists(self, path):
28 """See `Backend`."""
29 return os.path.exists(path)
30
31 def isdir(self, path):
32 """See `Backend`."""
33 return os.path.isdir(path)
34
35 def islink(self, path):
36 """See `Backend`."""
37 return os.path.islink(path)
38
39
40class PotteryGenerateIntltool(LaunchpadScript):
41 """Print a list of directories that contain a valid intltool structure."""
42
43 def add_my_options(self):
44 """See `LaunchpadScript`."""
45 self.parser.usage = "%prog [options] [PATH]"
46
47 def main(self):
48 """See `LaunchpadScript`."""
49 path = self.args[0] if self.args else "."
50 backend = UncontainedBackend("dummy")
51 print("\n".join(generate_pots(backend, path)))
52
53
54if __name__ == "__main__":
55 script = PotteryGenerateIntltool(name="pottery-generate-intltool")
56 script.run()
diff --git a/utilities/launchpad-database-setup b/utilities/launchpad-database-setup
index 46a36ce..83b79b4 100755
--- a/utilities/launchpad-database-setup
+++ b/utilities/launchpad-database-setup
@@ -43,13 +43,6 @@ if ! sudo grep -q "port.*5432" /etc/postgresql/$pgversion/main/postgresql.conf;
43 echo "ensure postgres is running on port 5432."43 echo "ensure postgres is running on port 5432."
44fi;44fi;
4545
46if [ -e /etc/init.d/postgresql-$pgversion ]; then
47 sudo /etc/init.d/postgresql-$pgversion stop
48else
49 # This is Maverick.
50 sudo /etc/init.d/postgresql stop $pgversion
51fi
52
53echo Purging postgresql data...46echo Purging postgresql data...
54sudo pg_dropcluster $pgversion main --stop-server47sudo pg_dropcluster $pgversion main --stop-server
55echo Re-creating postgresql database...48echo Re-creating postgresql database...
diff --git a/utilities/sourcedeps.cache b/utilities/sourcedeps.cache
index ff07c4c..ca3b453 100644
--- a/utilities/sourcedeps.cache
+++ b/utilities/sourcedeps.cache
@@ -24,8 +24,8 @@
24 "cjwatson@canonical.com-20190614154330-091l9edcnubsjmsx"24 "cjwatson@canonical.com-20190614154330-091l9edcnubsjmsx"
25 ],25 ],
26 "loggerhead": [26 "loggerhead": [
27 506,27 511,
28 "cjwatson@canonical.com-20200710095850-o3aa6eo5a22jhuun"28 "otto-copilot@canonical.com-20200918084828-dljpy2eewt6umnmd"
29 ],29 ],
30 "pygettextpo": [30 "pygettextpo": [
31 25,31 25,
diff --git a/utilities/sourcedeps.conf b/utilities/sourcedeps.conf
index 2377277..2815420 100644
--- a/utilities/sourcedeps.conf
+++ b/utilities/sourcedeps.conf
@@ -13,5 +13,5 @@ bzr-git lp:~launchpad-pqm/bzr-git/devel;revno=280
13bzr-svn lp:~launchpad-pqm/bzr-svn/devel;revno=272513bzr-svn lp:~launchpad-pqm/bzr-svn/devel;revno=2725
14cscvs lp:~launchpad-pqm/launchpad-cscvs/devel;revno=43314cscvs lp:~launchpad-pqm/launchpad-cscvs/devel;revno=433
15difftacular lp:~launchpad/difftacular/trunk;revno=1115difftacular lp:~launchpad/difftacular/trunk;revno=11
16loggerhead lp:~loggerhead-team/loggerhead/trunk-rich;revno=50616loggerhead lp:~loggerhead-team/loggerhead/trunk-rich;revno=511
17pygettextpo lp:~launchpad-pqm/pygettextpo/trunk;revno=2517pygettextpo lp:~launchpad-pqm/pygettextpo/trunk;revno=25

Subscribers

People subscribed via source and target branches

to status/vote changes: