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