Merge lp:~julian-edwards/launchpad/publisher-config-db-schema into lp:launchpad
- publisher-config-db-schema
- Merge into devel
Status: | Rejected |
---|---|
Rejected by: | Julian Edwards |
Proposed branch: | lp:~julian-edwards/launchpad/publisher-config-db-schema |
Merge into: | lp:launchpad |
Diff against target: |
1119 lines (+645/-45) 26 files modified
database/schema/launchpad_session.sql (+25/-0) database/schema/patch-2208-99-0.sql (+14/-0) database/schema/security.cfg (+1/-0) lib/lp/archivepublisher/config.py (+0/-4) lib/lp/archivepublisher/deathrow.py (+2/-7) lib/lp/archivepublisher/interfaces/publisherconfig.py (+58/-0) lib/lp/archivepublisher/model/publisherconfig.py (+68/-0) lib/lp/archivepublisher/publishing.py (+1/-6) lib/lp/archivepublisher/tests/test_publisherconfig.py (+66/-0) lib/lp/archivepublisher/zcml/configure.zcml (+23/-1) lib/lp/bugs/configure.zcml (+4/-0) lib/lp/bugs/doc/bugnotification-sending.txt (+9/-3) lib/lp/bugs/enum.py (+4/-4) lib/lp/bugs/mail/bugnotificationrecipients.py (+11/-0) lib/lp/bugs/model/bugnotification.py (+11/-0) lib/lp/bugs/model/structuralsubscription.py (+4/-3) lib/lp/scripts/garbo.py (+73/-8) lib/lp/scripts/tests/test_garbo.py (+104/-5) lib/lp/services/configure.zcml (+1/-0) lib/lp/services/session/adapters.py (+40/-0) lib/lp/services/session/configure.zcml (+12/-0) lib/lp/services/session/interfaces.py (+15/-0) lib/lp/services/session/model.py (+47/-0) lib/lp/services/session/tests/test_session.py (+32/-0) lib/lp/testing/factory.py (+15/-0) lib/lp/testing/tests/test_standard_test_template.py (+5/-4) |
To merge this branch: | bzr merge lp:~julian-edwards/launchpad/publisher-config-db-schema |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Julian Edwards (community) | Needs Resubmitting | ||
Benji York (community) | code | Approve | |
Robert Collins | db | Pending | |
Stuart Bishop | db | Pending | |
Review via email: mp+52411@code.launchpad.net |
Commit message
Description of the change
= Summary =
New PublisherConfig table
== Proposed fix ==
This branch adds a new PublisherConfig table which will eventually deprecate
the archivepublisher config section.
The schema allows for separate configurations for each hosted distribution
which is required as part of the Derived Distributions feature. When we start
hosting multiple distributions, the single configuration that currently exists
for the purposes of publishing Ubuntu will not suffice. The configured
options are:
* The base path for the archive
* The URL to the archive
It's entirely possible that custom distros will be hosted in an entirely
different disk area to Ubuntu, so we need to retain this configurability for
each distro.
The intention is to add a trivial LaunchpadEditForm page after this lands so
that we can set up the data, fix the rest of the code to use it, and delete
the original config.
== Implementation details ==
Fairly trivial schema change plus new model code.
== Tests ==
bin/test -cvv test_publisherc
== Demo and Q/A ==
n/a yet
Julian Edwards (julian-edwards) wrote : | # |
Benji, thanks to "bzr send" this MP is targeted to devel instead of db-devel.... I'm going to file a new one if you wouldn't mind blessing it!
Preview Diff
1 | === modified file 'database/schema/launchpad_session.sql' |
2 | --- database/schema/launchpad_session.sql 2010-09-10 09:45:45 +0000 |
3 | +++ database/schema/launchpad_session.sql 2011-03-07 15:37:44 +0000 |
4 | @@ -29,3 +29,28 @@ |
5 | GRANT SELECT, INSERT, UPDATE, DELETE ON TimeLimitedToken TO session; |
6 | -- And the garbo needs to run on it too. |
7 | GRANT SELECT, DELETE ON TimeLimitedToken TO session; |
8 | + |
9 | + |
10 | +-- This helper needs to exist in the session database so the BulkPruner |
11 | +-- can clean up unwanted sessions. |
12 | +CREATE OR REPLACE FUNCTION cursor_fetch(cur refcursor, n integer) |
13 | +RETURNS SETOF record LANGUAGE plpgsql AS |
14 | +$$ |
15 | +DECLARE |
16 | + r record; |
17 | + count integer; |
18 | +BEGIN |
19 | + FOR count IN 1..n LOOP |
20 | + FETCH FORWARD FROM cur INTO r; |
21 | + IF NOT FOUND THEN |
22 | + RETURN; |
23 | + END IF; |
24 | + RETURN NEXT r; |
25 | + END LOOP; |
26 | +END; |
27 | +$$; |
28 | + |
29 | +COMMENT ON FUNCTION cursor_fetch(refcursor, integer) IS |
30 | +'Fetch the next n items from a cursor. Work around for not being able to use FETCH inside a SELECT statement.'; |
31 | + |
32 | +GRANT EXECUTE ON FUNCTION cursor_fetch(refcursor, integer) TO session; |
33 | |
34 | === added file 'database/schema/patch-2208-99-0.sql' |
35 | --- database/schema/patch-2208-99-0.sql 1970-01-01 00:00:00 +0000 |
36 | +++ database/schema/patch-2208-99-0.sql 2011-03-07 15:37:44 +0000 |
37 | @@ -0,0 +1,14 @@ |
38 | +SET client_min_messages=ERROR; |
39 | + |
40 | +CREATE TABLE PublisherConfig ( |
41 | + id serial PRIMARY KEY, |
42 | + distribution integer NOT NULL CONSTRAINT publisherconfig__distribution__fk REFERENCES distribution, |
43 | + root_dir text NOT NULL, |
44 | + base_url text NOT NULL, |
45 | + copy_base_url text NOT NULL |
46 | +); |
47 | + |
48 | +CREATE UNIQUE INDEX publisherconfig__distribution__idx |
49 | + ON PublisherConfig(distribution); |
50 | + |
51 | +INSERT INTO LaunchpadDatabaseRevision VALUES (2208, 99, 0); |
52 | |
53 | === modified file 'database/schema/security.cfg' |
54 | --- database/schema/security.cfg 2011-03-07 07:27:56 +0000 |
55 | +++ database/schema/security.cfg 2011-03-07 15:37:44 +0000 |
56 | @@ -260,6 +260,7 @@ |
57 | public.productrelease = SELECT, INSERT, UPDATE, DELETE |
58 | public.productreleasefile = SELECT, INSERT, DELETE |
59 | public.productseriescodeimport = SELECT, INSERT, UPDATE |
60 | +public.publisherconfig = SELECT, INSERT, UPDATE, DELETE |
61 | public.project = SELECT |
62 | public.projectbounty = SELECT, INSERT, UPDATE |
63 | public.questionbug = SELECT, INSERT, DELETE |
64 | |
65 | === modified file 'lib/lp/archivepublisher/config.py' |
66 | --- lib/lp/archivepublisher/config.py 2010-10-17 13:35:20 +0000 |
67 | +++ lib/lp/archivepublisher/config.py 2011-03-07 15:37:44 +0000 |
68 | @@ -83,10 +83,6 @@ |
69 | return pubconf |
70 | |
71 | |
72 | -class LucilleConfigError(Exception): |
73 | - """Lucille configuration was not present.""" |
74 | - |
75 | - |
76 | class Config(object): |
77 | """Manage a publisher configuration from the database. (Read Only) |
78 | This class provides a useful abstraction so that if we change |
79 | |
80 | === modified file 'lib/lp/archivepublisher/deathrow.py' |
81 | --- lib/lp/archivepublisher/deathrow.py 2010-10-17 13:35:20 +0000 |
82 | +++ lib/lp/archivepublisher/deathrow.py 2011-03-07 15:37:44 +0000 |
83 | @@ -17,7 +17,6 @@ |
84 | from lp.archivepublisher import ELIGIBLE_DOMINATION_STATES |
85 | from lp.archivepublisher.config import ( |
86 | getPubConfig, |
87 | - LucilleConfigError, |
88 | ) |
89 | from lp.archivepublisher.diskpool import DiskPool |
90 | from lp.archivepublisher.utils import process_in_batches |
91 | @@ -40,12 +39,8 @@ |
92 | the one provided by the publishing-configuration, it will be only |
93 | used for PRIMARY archives. |
94 | """ |
95 | - log.debug("Grab Lucille config.") |
96 | - try: |
97 | - pubconf = getPubConfig(archive) |
98 | - except LucilleConfigError, info: |
99 | - log.error(info) |
100 | - raise |
101 | + log.debug("Grab publisher config.") |
102 | + pubconf = getPubConfig(archive) |
103 | |
104 | if (pool_root_override is not None and |
105 | archive.purpose == ArchivePurpose.PRIMARY): |
106 | |
107 | === added file 'lib/lp/archivepublisher/interfaces/publisherconfig.py' |
108 | --- lib/lp/archivepublisher/interfaces/publisherconfig.py 1970-01-01 00:00:00 +0000 |
109 | +++ lib/lp/archivepublisher/interfaces/publisherconfig.py 2011-03-07 15:37:44 +0000 |
110 | @@ -0,0 +1,58 @@ |
111 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
112 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
113 | + |
114 | +# pylint: disable-msg=E0211,E0213 |
115 | + |
116 | +"""PublisherConfig interface.""" |
117 | + |
118 | +__metaclass__ = type |
119 | + |
120 | +__all__ = [ |
121 | + 'IPublisherConfig', |
122 | + 'IPublisherConfigSet', |
123 | + ] |
124 | + |
125 | +from lazr.restful.fields import Reference |
126 | +from zope.interface import Interface |
127 | +from zope.schema import ( |
128 | + Int, |
129 | + TextLine, |
130 | + ) |
131 | + |
132 | +from canonical.launchpad import _ |
133 | +from lp.registry.interfaces.distribution import IDistribution |
134 | + |
135 | + |
136 | +class IPublisherConfig(Interface): |
137 | + """`PublisherConfig` interface.""" |
138 | + |
139 | + id = Int(title=_('ID'), required=True, readonly=True) |
140 | + |
141 | + distribution = Reference( |
142 | + IDistribution, title=_("Distribution"), required=True, |
143 | + description=_("The Distribution for this configuration.")) |
144 | + |
145 | + root_dir = TextLine( |
146 | + title=_("Root Directory"), required=True, |
147 | + description=_("The root directory for published archives.")) |
148 | + |
149 | + base_url = TextLine( |
150 | + title=_("Base URL"), required=True, |
151 | + description=_("The base URL for published archives")) |
152 | + |
153 | + copy_base_url = TextLine( |
154 | + title=_("Copy Base URL"), required=True, |
155 | + description=_("The base URL for published copy archives")) |
156 | + |
157 | + |
158 | +class IPublisherConfigSet(Interface): |
159 | + """`PublisherConfigSet` interface.""" |
160 | + |
161 | + def new(distribution, root_dir, base_url, copy_base_url): |
162 | + """Create a new `PublisherConfig`.""" |
163 | + |
164 | + def getByDistribution(distribution): |
165 | + """Get the config for a a distribution. |
166 | + |
167 | + :param distribution: An `IDistribution` |
168 | + """ |
169 | |
170 | === added file 'lib/lp/archivepublisher/model/publisherconfig.py' |
171 | --- lib/lp/archivepublisher/model/publisherconfig.py 1970-01-01 00:00:00 +0000 |
172 | +++ lib/lp/archivepublisher/model/publisherconfig.py 2011-03-07 15:37:44 +0000 |
173 | @@ -0,0 +1,68 @@ |
174 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
175 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
176 | + |
177 | +"""Database class for table PublisherConfig.""" |
178 | + |
179 | +__metaclass__ = type |
180 | + |
181 | +__all__ = [ |
182 | + 'PublisherConfig', |
183 | + 'PublisherConfigSet', |
184 | + ] |
185 | + |
186 | +from storm.locals import ( |
187 | + Int, |
188 | + RawStr, |
189 | + Reference, |
190 | + Storm, |
191 | + ) |
192 | +from zope.interface import implements |
193 | + |
194 | +from canonical.launchpad.interfaces.lpstorm import ( |
195 | + IMasterStore, |
196 | + ) |
197 | +from lp.archivepublisher.interfaces.publisherconfig import ( |
198 | + IPublisherConfig, |
199 | + IPublisherConfigSet, |
200 | + ) |
201 | + |
202 | + |
203 | +class PublisherConfig(Storm): |
204 | + """See `IArchiveAuthToken`.""" |
205 | + implements(IPublisherConfig) |
206 | + __storm_table__ = 'PublisherConfig' |
207 | + |
208 | + id = Int(primary=True) |
209 | + |
210 | + distribution_id = Int(name='distribution', allow_none=False) |
211 | + distribution = Reference(distribution_id, 'Distribution.id') |
212 | + |
213 | + root_dir = RawStr(name='root_dir', allow_none=False) |
214 | + |
215 | + base_url = RawStr(name='base_url', allow_none=False) |
216 | + |
217 | + copy_base_url = RawStr(name='copy_base_url', allow_none=False) |
218 | + |
219 | + |
220 | +class PublisherConfigSet: |
221 | + """See `IPublisherConfigSet`.""" |
222 | + implements(IPublisherConfigSet) |
223 | + title = "Soyuz Publisher Configurations" |
224 | + |
225 | + def new(self, distribution, root_dir, base_url, copy_base_url): |
226 | + """Make and return a new `PublisherConfig`.""" |
227 | + store = IMasterStore(PublisherConfig) |
228 | + pubconf = PublisherConfig() |
229 | + pubconf.distribution = distribution |
230 | + pubconf.root_dir = root_dir |
231 | + pubconf.base_url = base_url |
232 | + pubconf.copy_base_url = copy_base_url |
233 | + store.add(pubconf) |
234 | + return pubconf |
235 | + |
236 | + def getByDistribution(self, distribution): |
237 | + """See `IArchiveAuthTokenSet`.""" |
238 | + store = IMasterStore(PublisherConfig) |
239 | + return store.find( |
240 | + PublisherConfig, |
241 | + PublisherConfig.distribution_id == distribution.id).one() |
242 | |
243 | === modified file 'lib/lp/archivepublisher/publishing.py' |
244 | --- lib/lp/archivepublisher/publishing.py 2011-02-04 09:07:36 +0000 |
245 | +++ lib/lp/archivepublisher/publishing.py 2011-03-07 15:37:44 +0000 |
246 | @@ -21,7 +21,6 @@ |
247 | from lp.archivepublisher import HARDCODED_COMPONENT_ORDER |
248 | from lp.archivepublisher.config import ( |
249 | getPubConfig, |
250 | - LucilleConfigError, |
251 | ) |
252 | from lp.archivepublisher.diskpool import DiskPool |
253 | from lp.archivepublisher.domination import Dominator |
254 | @@ -120,11 +119,7 @@ |
255 | else: |
256 | log.debug("Finding configuration for '%s' PPA." |
257 | % archive.owner.name) |
258 | - try: |
259 | - pubconf = getPubConfig(archive) |
260 | - except LucilleConfigError, info: |
261 | - log.error(info) |
262 | - raise |
263 | + pubconf = getPubConfig(archive) |
264 | |
265 | disk_pool = _getDiskPool(pubconf, log) |
266 | |
267 | |
268 | === added file 'lib/lp/archivepublisher/tests/test_publisherconfig.py' |
269 | --- lib/lp/archivepublisher/tests/test_publisherconfig.py 1970-01-01 00:00:00 +0000 |
270 | +++ lib/lp/archivepublisher/tests/test_publisherconfig.py 2011-03-07 15:37:44 +0000 |
271 | @@ -0,0 +1,66 @@ |
272 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
273 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
274 | + |
275 | +"""Tests for publisherConfig model class.""" |
276 | + |
277 | +__metaclass__ = type |
278 | + |
279 | + |
280 | +from storm.store import Store |
281 | +from storm.exceptions import IntegrityError |
282 | +from zope.component import getUtility |
283 | +from zope.interface.verify import verifyObject |
284 | + |
285 | +from canonical.testing.layers import ZopelessDatabaseLayer |
286 | +from lp.archivepublisher.interfaces.publisherconfig import ( |
287 | + IPublisherConfig, |
288 | + IPublisherConfigSet, |
289 | + ) |
290 | +from lp.testing import TestCaseWithFactory |
291 | + |
292 | + |
293 | +class TestPublisherConfig(TestCaseWithFactory): |
294 | + """Test the `PublisherConfig` model.""" |
295 | + layer = ZopelessDatabaseLayer |
296 | + |
297 | + def setUp(self): |
298 | + TestCaseWithFactory.setUp(self) |
299 | + self.distribution = self.factory.makeDistribution(name='conftest') |
300 | + |
301 | + def test_verify_interface(self): |
302 | + # Test the interface for the model. |
303 | + pubconf = self.factory.makePublisherConfig() |
304 | + verified = verifyObject(IPublisherConfig, pubconf) |
305 | + self.assertTrue(verified) |
306 | + |
307 | + def test_properties(self): |
308 | + # Test the model properties. |
309 | + ROOT_DIR = "rootdir/test" |
310 | + BASE_URL = "http://base.url" |
311 | + COPY_BASE_URL = "http://base.url" |
312 | + pubconf = self.factory.makePublisherConfig( |
313 | + distribution=self.distribution, |
314 | + root_dir=ROOT_DIR, |
315 | + base_url=BASE_URL, |
316 | + copy_base_url=COPY_BASE_URL, |
317 | + ) |
318 | + |
319 | + self.assertEqual(self.distribution.name, pubconf.distribution.name) |
320 | + self.assertEqual(ROOT_DIR, pubconf.root_dir) |
321 | + self.assertEqual(BASE_URL, pubconf.base_url) |
322 | + self.assertEqual(COPY_BASE_URL, pubconf.copy_base_url) |
323 | + |
324 | + def test_one_config_per_distro(self): |
325 | + # Only one config for each distro is allowed. |
326 | + pubconf = self.factory.makePublisherConfig(self.distribution) |
327 | + pubconf2 = self.factory.makePublisherConfig(self.distribution) |
328 | + store = Store.of(pubconf) |
329 | + self.assertRaises(IntegrityError, store.flush) |
330 | + |
331 | + def test_getByDistribution(self): |
332 | + # Test that IPublisherConfigSet.getByDistribution works. |
333 | + pubconf = self.factory.makePublisherConfig( |
334 | + distribution=self.distribution) |
335 | + pubconf = getUtility(IPublisherConfigSet).getByDistribution( |
336 | + self.distribution) |
337 | + self.assertEqual(self.distribution.name, pubconf.distribution.name) |
338 | |
339 | === modified file 'lib/lp/archivepublisher/zcml/configure.zcml' |
340 | --- lib/lp/archivepublisher/zcml/configure.zcml 2009-07-13 18:15:02 +0000 |
341 | +++ lib/lp/archivepublisher/zcml/configure.zcml 2011-03-07 15:37:44 +0000 |
342 | @@ -2,8 +2,30 @@ |
343 | GNU Affero General Public License version 3 (see the file LICENSE). |
344 | --> |
345 | |
346 | -<configure xmlns="http://namespaces.zope.org/zope"> |
347 | +<configure |
348 | + xmlns="http://namespaces.zope.org/zope" |
349 | + xmlns:browser="http://namespaces.zope.org/browser" |
350 | + xmlns:i18n="http://namespaces.zope.org/i18n" |
351 | + xmlns:webservice="http://namespaces.canonical.com/webservice" |
352 | + xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc" |
353 | + i18n_domain="launchpad"> |
354 | + |
355 | <include package="lp.archivepublisher.zcml" |
356 | file="archivesigningkey.zcml" /> |
357 | + |
358 | + <securedutility |
359 | + class="lp.archivepublisher.model.publisherconfig.PublisherConfigSet" |
360 | + provides="lp.archivepublisher.interfaces.publisherconfig.IPublisherConfigSet"> |
361 | + <allow |
362 | + interface="lp.archivepublisher.interfaces.publisherconfig.IPublisherConfigSet"/> |
363 | + </securedutility> |
364 | + |
365 | + <class |
366 | + class="lp.archivepublisher.model.publisherconfig.PublisherConfig"> |
367 | + <allow |
368 | + interface="lp.archivepublisher.interfaces.publisherconfig.IPublisherConfig" /> |
369 | + </class> |
370 | + |
371 | + |
372 | </configure> |
373 | |
374 | |
375 | === modified file 'lib/lp/bugs/configure.zcml' |
376 | --- lib/lp/bugs/configure.zcml 2011-03-02 23:08:54 +0000 |
377 | +++ lib/lp/bugs/configure.zcml 2011-03-07 15:37:44 +0000 |
378 | @@ -1056,6 +1056,10 @@ |
379 | class="lp.bugs.mail.bugnotificationrecipients.BugNotificationRecipients"> |
380 | <allow |
381 | interface="canonical.launchpad.interfaces.launchpad.INotificationRecipientSet"/> |
382 | + <!-- BugNotificationRecipients provides the following |
383 | + attributes/methods in addition. --> |
384 | + <allow |
385 | + attributes="subscription_filters addFilter"/> |
386 | </class> |
387 | <securedutility |
388 | provides="lp.bugs.interfaces.bugnotification.IBugNotificationSet" |
389 | |
390 | === modified file 'lib/lp/bugs/doc/bugnotification-sending.txt' |
391 | --- lib/lp/bugs/doc/bugnotification-sending.txt 2011-02-22 10:44:48 +0000 |
392 | +++ lib/lp/bugs/doc/bugnotification-sending.txt 2011-03-07 15:37:44 +0000 |
393 | @@ -21,8 +21,10 @@ |
394 | |
395 | >>> def print_notification_headers(email_notification): |
396 | ... for header in ['To', 'From', 'Subject', |
397 | - ... 'X-Launchpad-Message-Rationale']: |
398 | - ... print "%s: %s" % (header, email_notification[header]) |
399 | + ... 'X-Launchpad-Message-Rationale', |
400 | + ... 'X-Launchpad-Subscription-Filter']: |
401 | + ... if email_notification[header]: |
402 | + ... print "%s: %s" % (header, email_notification[header]) |
403 | |
404 | >>> def print_notification(email_notification): |
405 | ... print_notification_headers(email_notification) |
406 | @@ -676,7 +678,6 @@ |
407 | been set properly. |
408 | |
409 | >>> from lp.bugs.model.bugnotification import BugNotification |
410 | - >>> from lp.bugs.enum import BugNotificationStatus |
411 | >>> for notification in BugNotification.select(orderBy='id')[-8:]: |
412 | ... if notification.is_comment: |
413 | ... identifier = 'comment' |
414 | @@ -1247,6 +1248,7 @@ |
415 | >>> with lp_dbuser(): |
416 | ... filter = subscription_no_priv.newBugFilter() |
417 | ... filter.bug_notification_level = BugNotificationLevel.COMMENTS |
418 | + ... filter.description = u"Allow-comments filter" |
419 | |
420 | >>> comment = getUtility(IMessageSet).fromText( |
421 | ... 'subject', 'another comment.', sample_person, |
422 | @@ -1279,12 +1281,14 @@ |
423 | From: Sample Person <...@bugs.launchpad.net> |
424 | Subject: [Bug 1] subject |
425 | X-Launchpad-Message-Rationale: Subscriber (Mozilla Firefox) |
426 | + X-Launchpad-Subscription-Filter: Allow-comments filter |
427 | <BLANKLINE> |
428 | another comment. |
429 | <BLANKLINE> |
430 | -- |
431 | You received this bug notification because you are subscribed to Mozilla |
432 | Firefox. |
433 | + Matching filters: Allow-comments filter |
434 | ... |
435 | ---------------------------------------------------------------------- |
436 | To: support@ubuntu.com |
437 | @@ -1390,6 +1394,7 @@ |
438 | From: Sample Person <...@bugs.launchpad.net> |
439 | Subject: [Bug 1] subject |
440 | X-Launchpad-Message-Rationale: Subscriber (Mozilla Firefox) |
441 | + X-Launchpad-Subscription-Filter: Allow-comments filter |
442 | <BLANKLINE> |
443 | no comment for no-priv. |
444 | <BLANKLINE> |
445 | @@ -1400,6 +1405,7 @@ |
446 | -- |
447 | You received this bug notification because you are subscribed to Mozilla |
448 | Firefox. |
449 | + Matching filters: Allow-comments filter |
450 | ... |
451 | ---------------------------------------------------------------------- |
452 | To: support@ubuntu.com |
453 | |
454 | === modified file 'lib/lp/bugs/enum.py' |
455 | --- lib/lp/bugs/enum.py 2011-03-05 00:06:26 +0000 |
456 | +++ lib/lp/bugs/enum.py 2011-03-07 15:37:44 +0000 |
457 | @@ -57,18 +57,18 @@ |
458 | |
459 | class BugNotificationStatus(DBEnumeratedType): |
460 | """The status of a bug notification. |
461 | - |
462 | + |
463 | A notification may be pending, sent, or omitted.""" |
464 | |
465 | PENDING = DBItem(10, """ |
466 | Pending |
467 | - |
468 | + |
469 | The notification has not yet been sent. |
470 | """) |
471 | |
472 | OMITTED = DBItem(20, """ |
473 | Omitted |
474 | - |
475 | + |
476 | The system considered sending the notification, but omitted it. |
477 | This is generally because the action reported by the notification |
478 | was immediately undone. |
479 | @@ -76,6 +76,6 @@ |
480 | |
481 | SENT = DBItem(30, """ |
482 | Sent |
483 | - |
484 | + |
485 | The notification has been sent. |
486 | """) |
487 | |
488 | === modified file 'lib/lp/bugs/mail/bugnotificationrecipients.py' |
489 | --- lib/lp/bugs/mail/bugnotificationrecipients.py 2011-03-02 14:07:23 +0000 |
490 | +++ lib/lp/bugs/mail/bugnotificationrecipients.py 2011-03-07 15:37:44 +0000 |
491 | @@ -58,6 +58,7 @@ |
492 | """ |
493 | NotificationRecipientSet.__init__(self) |
494 | self.duplicateof = duplicateof |
495 | + self.subscription_filters = set() |
496 | |
497 | def _addReason(self, person, reason, header): |
498 | """Adds a reason (text and header) for a person. |
499 | @@ -127,3 +128,13 @@ |
500 | else: |
501 | text = "are the registrant for %s" % upstream.displayname |
502 | self._addReason(person, text, reason) |
503 | + |
504 | + def update(self, recipient_set): |
505 | + """See `INotificationRecipientSet`.""" |
506 | + super(BugNotificationRecipients, self).update(recipient_set) |
507 | + self.subscription_filters.update( |
508 | + recipient_set.subscription_filters) |
509 | + |
510 | + def addFilter(self, subscription_filter): |
511 | + if subscription_filter is not None: |
512 | + self.subscription_filters.add(subscription_filter) |
513 | |
514 | === modified file 'lib/lp/bugs/model/bugnotification.py' |
515 | --- lib/lp/bugs/model/bugnotification.py 2011-02-25 16:19:58 +0000 |
516 | +++ lib/lp/bugs/model/bugnotification.py 2011-03-07 15:37:44 +0000 |
517 | @@ -150,12 +150,23 @@ |
518 | reason_body, reason_header = recipients.getReason(recipient) |
519 | sql_values.append('(%s, %s, %s, %s)' % sqlvalues( |
520 | bug_notification, recipient, reason_header, reason_body)) |
521 | + |
522 | # We add all the recipients in a single SQL statement to make |
523 | # this a bit more efficient for bugs with many subscribers. |
524 | store.execute(""" |
525 | INSERT INTO BugNotificationRecipient |
526 | (bug_notification, person, reason_header, reason_body) |
527 | VALUES %s;""" % ', '.join(sql_values)) |
528 | + |
529 | + if len(recipients.subscription_filters) > 0: |
530 | + filter_link_sql = [ |
531 | + "(%s, %s)" % sqlvalues(bug_notification, filter.id) |
532 | + for filter in recipients.subscription_filters] |
533 | + store.execute(""" |
534 | + INSERT INTO BugNotificationFilter |
535 | + (bug_notification, bug_subscription_filter) |
536 | + VALUES %s;""" % ", ".join(filter_link_sql)) |
537 | + |
538 | return bug_notification |
539 | |
540 | |
541 | |
542 | === modified file 'lib/lp/bugs/model/structuralsubscription.py' |
543 | --- lib/lp/bugs/model/structuralsubscription.py 2011-03-04 02:29:09 +0000 |
544 | +++ lib/lp/bugs/model/structuralsubscription.py 2011-03-07 15:37:44 +0000 |
545 | @@ -585,14 +585,15 @@ |
546 | else: |
547 | subscribers = [] |
548 | query_results = source.find( |
549 | - (Person, StructuralSubscription), |
550 | - *constraints).config(distinct=True) |
551 | - for person, subscription in query_results: |
552 | + (Person, StructuralSubscription, BugSubscriptionFilter), |
553 | + *constraints) |
554 | + for person, subscription, filter in query_results: |
555 | # Set up results. |
556 | if person not in recipients: |
557 | subscribers.append(person) |
558 | recipients.addStructuralSubscriber( |
559 | person, subscription.target) |
560 | + recipients.addFilter(filter) |
561 | return subscribers |
562 | |
563 | |
564 | |
565 | === modified file 'lib/lp/scripts/garbo.py' |
566 | --- lib/lp/scripts/garbo.py 2011-02-23 10:28:53 +0000 |
567 | +++ lib/lp/scripts/garbo.py 2011-03-07 15:37:44 +0000 |
568 | @@ -74,6 +74,7 @@ |
569 | LaunchpadCronScript, |
570 | SilentLaunchpadScriptFailure, |
571 | ) |
572 | +from lp.services.session.model import SessionData |
573 | from lp.translations.interfaces.potemplate import IPOTemplateSet |
574 | from lp.translations.model.potranslation import POTranslation |
575 | |
576 | @@ -107,10 +108,14 @@ |
577 | # from. Must be overridden. |
578 | target_table_class = None |
579 | |
580 | - # The column name in target_table we use as the integer key. May be |
581 | - # overridden. |
582 | + # The column name in target_table we use as the key. The type must |
583 | + # match that returned by the ids_to_prune_query and the |
584 | + # target_table_key_type. May be overridden. |
585 | target_table_key = 'id' |
586 | |
587 | + # SQL type of the target_table_key. May be overridden. |
588 | + target_table_key_type = 'integer' |
589 | + |
590 | # An SQL query returning a list of ids to remove from target_table. |
591 | # The query must return a single column named 'id' and should not |
592 | # contain duplicates. Must be overridden. |
593 | @@ -119,10 +124,23 @@ |
594 | # See `TunableLoop`. May be overridden. |
595 | maximum_chunk_size = 10000 |
596 | |
597 | + # Optional extra WHERE clause fragment for the deletion to skip |
598 | + # arbitrary rows flagged for deletion. For example, skip rows |
599 | + # that might have been modified since the set of ids_to_prune |
600 | + # was calculated. |
601 | + extra_prune_clause = None |
602 | + |
603 | + def getStore(self): |
604 | + """The master Store for the table we are pruning. |
605 | + |
606 | + May be overridden. |
607 | + """ |
608 | + return IMasterStore(self.target_table_class) |
609 | + |
610 | def __init__(self, log, abort_time=None): |
611 | super(BulkPruner, self).__init__(log, abort_time) |
612 | |
613 | - self.store = IMasterStore(self.target_table_class) |
614 | + self.store = self.getStore() |
615 | self.target_table_name = self.target_table_class.__storm_table__ |
616 | |
617 | # Open the cursor. |
618 | @@ -138,12 +156,20 @@ |
619 | |
620 | def __call__(self, chunk_size): |
621 | """See `ITunableLoop`.""" |
622 | + if self.extra_prune_clause: |
623 | + extra = "AND (%s)" % self.extra_prune_clause |
624 | + else: |
625 | + extra = "" |
626 | result = self.store.execute(""" |
627 | - DELETE FROM %s WHERE %s IN ( |
628 | + DELETE FROM %s |
629 | + WHERE %s IN ( |
630 | SELECT id FROM |
631 | - cursor_fetch('bulkprunerid', %d) AS f(id integer)) |
632 | + cursor_fetch('bulkprunerid', %d) AS f(id %s)) |
633 | + %s |
634 | """ |
635 | - % (self.target_table_name, self.target_table_key, chunk_size)) |
636 | + % ( |
637 | + self.target_table_name, self.target_table_key, |
638 | + chunk_size, self.target_table_key_type, extra)) |
639 | self._num_removed = result.rowcount |
640 | transaction.commit() |
641 | |
642 | @@ -157,9 +183,7 @@ |
643 | |
644 | XXX bug=723596 StuartBishop: This job only needs to run once per month. |
645 | """ |
646 | - |
647 | target_table_class = POTranslation |
648 | - |
649 | ids_to_prune_query = """ |
650 | SELECT POTranslation.id AS id FROM POTranslation |
651 | EXCEPT ( |
652 | @@ -186,6 +210,45 @@ |
653 | """ |
654 | |
655 | |
656 | +class SessionPruner(BulkPruner): |
657 | + """Base class for session removal.""" |
658 | + |
659 | + target_table_class = SessionData |
660 | + target_table_key = 'client_id' |
661 | + target_table_key_type = 'text' |
662 | + |
663 | + |
664 | +class AntiqueSessionPruner(SessionPruner): |
665 | + """Remove sessions not accessed for 60 days""" |
666 | + |
667 | + ids_to_prune_query = """ |
668 | + SELECT client_id AS id FROM SessionData |
669 | + WHERE last_accessed < CURRENT_TIMESTAMP - CAST('60 days' AS interval) |
670 | + """ |
671 | + |
672 | + |
673 | +class UnusedSessionPruner(SessionPruner): |
674 | + """Remove sessions older than 1 day with no authentication credentials.""" |
675 | + |
676 | + ids_to_prune_query = """ |
677 | + SELECT client_id AS id FROM SessionData |
678 | + WHERE |
679 | + last_accessed < CURRENT_TIMESTAMP - CAST('1 day' AS interval) |
680 | + AND client_id NOT IN ( |
681 | + SELECT client_id |
682 | + FROM SessionPkgData |
683 | + WHERE |
684 | + product_id = 'launchpad.authenticateduser' |
685 | + AND key='logintime') |
686 | + """ |
687 | + |
688 | + # Don't delete a session if it has been used between calculating |
689 | + # the list of sessions to remove and the current iteration. |
690 | + prune_extra_clause = """ |
691 | + last_accessed < CURRENT_TIMESTAMP - CAST('1 day' AS interval) |
692 | + """ |
693 | + |
694 | + |
695 | class OAuthNoncePruner(TunableLoop): |
696 | """An ITunableLoop to prune old OAuthNonce records. |
697 | |
698 | @@ -977,6 +1040,8 @@ |
699 | RevisionCachePruner, |
700 | BugHeatUpdater, |
701 | BugWatchScheduler, |
702 | + AntiqueSessionPruner, |
703 | + UnusedSessionPruner, |
704 | ] |
705 | experimental_tunable_loops = [] |
706 | |
707 | |
708 | === modified file 'lib/lp/scripts/tests/test_garbo.py' |
709 | --- lib/lp/scripts/tests/test_garbo.py 2011-02-28 11:50:34 +0000 |
710 | +++ lib/lp/scripts/tests/test_garbo.py 2011-03-07 15:37:44 +0000 |
711 | @@ -10,7 +10,6 @@ |
712 | datetime, |
713 | timedelta, |
714 | ) |
715 | -import operator |
716 | import time |
717 | |
718 | from pytz import UTC |
719 | @@ -18,7 +17,10 @@ |
720 | Min, |
721 | SQL, |
722 | ) |
723 | -from storm.locals import Storm, Int |
724 | +from storm.locals import ( |
725 | + Int, |
726 | + Storm, |
727 | + ) |
728 | from storm.store import Store |
729 | import transaction |
730 | from zope.component import getUtility |
731 | @@ -69,13 +71,19 @@ |
732 | PersonCreationRationale, |
733 | ) |
734 | from lp.scripts.garbo import ( |
735 | + AntiqueSessionPruner, |
736 | BulkPruner, |
737 | DailyDatabaseGarbageCollector, |
738 | HourlyDatabaseGarbageCollector, |
739 | OpenIDConsumerAssociationPruner, |
740 | + UnusedSessionPruner, |
741 | ) |
742 | from lp.services.job.model.job import Job |
743 | from lp.services.log.logger import BufferLogger |
744 | +from lp.services.session.model import ( |
745 | + SessionData, |
746 | + SessionPkgData, |
747 | + ) |
748 | from lp.testing import ( |
749 | TestCase, |
750 | TestCaseWithFactory, |
751 | @@ -181,6 +189,97 @@ |
752 | pruner(chunk_size) |
753 | |
754 | |
755 | +class TestSessionPruner(TestCase): |
756 | + layer = ZopelessDatabaseLayer |
757 | + |
758 | + def setUp(self): |
759 | + super(TestCase, self).setUp() |
760 | + |
761 | + # Session database isn't reset between tests. We need to do this |
762 | + # manually. |
763 | + nuke_all_sessions = IMasterStore(SessionData).find(SessionData).remove |
764 | + nuke_all_sessions() |
765 | + self.addCleanup(nuke_all_sessions) |
766 | + |
767 | + recent = datetime.now(UTC) |
768 | + yesterday = recent - timedelta(days=1) |
769 | + ancient = recent - timedelta(days=61) |
770 | + |
771 | + def make_session(client_id, accessed, authenticated=None): |
772 | + session_data = SessionData() |
773 | + session_data.client_id = client_id |
774 | + session_data.last_accessed = accessed |
775 | + IMasterStore(SessionData).add(session_data) |
776 | + |
777 | + if authenticated: |
778 | + session_pkg_data = SessionPkgData() |
779 | + session_pkg_data.client_id = client_id |
780 | + session_pkg_data.product_id = u'launchpad.authenticateduser' |
781 | + session_pkg_data.key = u'logintime' |
782 | + session_pkg_data.pickle = 'value is ignored' |
783 | + IMasterStore(SessionPkgData).add(session_pkg_data) |
784 | + |
785 | + make_session(u'recent_auth', recent, True) |
786 | + make_session(u'recent_unauth', recent, False) |
787 | + make_session(u'yesterday_auth', yesterday, True) |
788 | + make_session(u'yesterday_unauth', yesterday, False) |
789 | + make_session(u'ancient_auth', ancient, True) |
790 | + make_session(u'ancient_unauth', ancient, False) |
791 | + |
792 | + def sessionExists(self, client_id): |
793 | + store = IMasterStore(SessionData) |
794 | + return not store.find( |
795 | + SessionData, SessionData.client_id == client_id).is_empty() |
796 | + |
797 | + def test_antique_session_pruner(self): |
798 | + chunk_size = 2 |
799 | + log = BufferLogger() |
800 | + pruner = AntiqueSessionPruner(log) |
801 | + try: |
802 | + while not pruner.isDone(): |
803 | + pruner(chunk_size) |
804 | + finally: |
805 | + pruner.cleanUp() |
806 | + |
807 | + expected_sessions = set([ |
808 | + u'recent_auth', |
809 | + u'recent_unauth', |
810 | + u'yesterday_auth', |
811 | + u'yesterday_unauth', |
812 | + # u'ancient_auth', |
813 | + # u'ancient_unauth', |
814 | + ]) |
815 | + |
816 | + found_sessions = set( |
817 | + IMasterStore(SessionData).find(SessionData.client_id)) |
818 | + |
819 | + self.assertEqual(expected_sessions, found_sessions) |
820 | + |
821 | + def test_unused_session_pruner(self): |
822 | + chunk_size = 2 |
823 | + log = BufferLogger() |
824 | + pruner = UnusedSessionPruner(log) |
825 | + try: |
826 | + while not pruner.isDone(): |
827 | + pruner(chunk_size) |
828 | + finally: |
829 | + pruner.cleanUp() |
830 | + |
831 | + expected_sessions = set([ |
832 | + u'recent_auth', |
833 | + u'recent_unauth', |
834 | + u'yesterday_auth', |
835 | + # u'yesterday_unauth', |
836 | + u'ancient_auth', |
837 | + # u'ancient_unauth', |
838 | + ]) |
839 | + |
840 | + found_sessions = set( |
841 | + IMasterStore(SessionData).find(SessionData.client_id)) |
842 | + |
843 | + self.assertEqual(expected_sessions, found_sessions) |
844 | + |
845 | + |
846 | class TestGarbo(TestCaseWithFactory): |
847 | layer = LaunchpadZopelessLayer |
848 | |
849 | @@ -209,7 +308,7 @@ |
850 | return collector |
851 | |
852 | def test_OAuthNoncePruner(self): |
853 | - now = datetime.utcnow().replace(tzinfo=UTC) |
854 | + now = datetime.now(UTC) |
855 | timestamps = [ |
856 | now - timedelta(days=2), # Garbage |
857 | now - timedelta(days=1) - timedelta(seconds=60), # Garbage |
858 | @@ -287,7 +386,7 @@ |
859 | self.failUnless(earliest >= now - 24*60*60, 'Still have old nonces') |
860 | |
861 | def test_CodeImportResultPruner(self): |
862 | - now = datetime.utcnow().replace(tzinfo=UTC) |
863 | + now = datetime.now(UTC) |
864 | store = IMasterStore(CodeImportResult) |
865 | |
866 | results_to_keep_count = ( |
867 | @@ -344,7 +443,7 @@ |
868 | >= now - timedelta(days=30)) |
869 | |
870 | def test_CodeImportEventPruner(self): |
871 | - now = datetime.utcnow().replace(tzinfo=UTC) |
872 | + now = datetime.now(UTC) |
873 | store = IMasterStore(CodeImportResult) |
874 | |
875 | LaunchpadZopelessLayer.switchDbUser('testadmin') |
876 | |
877 | === modified file 'lib/lp/services/configure.zcml' |
878 | --- lib/lp/services/configure.zcml 2011-03-04 17:05:09 +0000 |
879 | +++ lib/lp/services/configure.zcml 2011-03-07 15:37:44 +0000 |
880 | @@ -16,5 +16,6 @@ |
881 | <include package=".salesforce" /> |
882 | <include package=".scripts" /> |
883 | <include package=".search" /> |
884 | + <include package=".session" /> |
885 | <include package=".worlddata" /> |
886 | </configure> |
887 | |
888 | === added directory 'lib/lp/services/session' |
889 | === added file 'lib/lp/services/session/__init__.py' |
890 | === added file 'lib/lp/services/session/adapters.py' |
891 | --- lib/lp/services/session/adapters.py 1970-01-01 00:00:00 +0000 |
892 | +++ lib/lp/services/session/adapters.py 2011-03-07 15:37:44 +0000 |
893 | @@ -0,0 +1,40 @@ |
894 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
895 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
896 | + |
897 | +"""Session adapters.""" |
898 | + |
899 | +__metaclass__ = type |
900 | +__all__ = [] |
901 | + |
902 | + |
903 | +from zope.component import adapter |
904 | +from zope.interface import implementer |
905 | + |
906 | +from canonical.database.sqlbase import session_store |
907 | +from canonical.launchpad.interfaces.lpstorm import ( |
908 | + IMasterStore, |
909 | + ISlaveStore, |
910 | + IStore, |
911 | + ) |
912 | +from lp.services.session.interfaces import IUseSessionStore |
913 | + |
914 | + |
915 | +@adapter(IUseSessionStore) |
916 | +@implementer(IMasterStore) |
917 | +def session_master_store(cls): |
918 | + """Adapt a Session database object to an `IMasterStore`.""" |
919 | + return session_store() |
920 | + |
921 | + |
922 | +@adapter(IUseSessionStore) |
923 | +@implementer(ISlaveStore) |
924 | +def session_slave_store(cls): |
925 | + """Adapt a Session database object to an `ISlaveStore`.""" |
926 | + return session_store() |
927 | + |
928 | + |
929 | +@adapter(IUseSessionStore) |
930 | +@implementer(IStore) |
931 | +def session_default_store(cls): |
932 | + """Adapt an Session database object to an `IStore`.""" |
933 | + return session_store() |
934 | |
935 | === added file 'lib/lp/services/session/configure.zcml' |
936 | --- lib/lp/services/session/configure.zcml 1970-01-01 00:00:00 +0000 |
937 | +++ lib/lp/services/session/configure.zcml 2011-03-07 15:37:44 +0000 |
938 | @@ -0,0 +1,12 @@ |
939 | +<!-- Copyright 2011 Canonical Ltd. This software is licensed under the |
940 | + GNU Affero General Public License version 3 (see the file LICENSE). |
941 | +--> |
942 | +<configure |
943 | + xmlns="http://namespaces.zope.org/zope" |
944 | + xmlns:browser="http://namespaces.zope.org/browser" |
945 | + xmlns:i18n="http://namespaces.zope.org/i18n" |
946 | + i18n_domain="launchpad"> |
947 | + <adapter factory=".adapters.session_master_store" /> |
948 | + <adapter factory=".adapters.session_slave_store" /> |
949 | + <adapter factory=".adapters.session_default_store" /> |
950 | +</configure> |
951 | |
952 | === added file 'lib/lp/services/session/interfaces.py' |
953 | --- lib/lp/services/session/interfaces.py 1970-01-01 00:00:00 +0000 |
954 | +++ lib/lp/services/session/interfaces.py 2011-03-07 15:37:44 +0000 |
955 | @@ -0,0 +1,15 @@ |
956 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
957 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
958 | + |
959 | +"""Session interfaces.""" |
960 | + |
961 | +__metaclass__ = type |
962 | +__all__ = ['IUseSessionStore'] |
963 | + |
964 | + |
965 | +from zope.interface import Interface |
966 | + |
967 | + |
968 | +class IUseSessionStore(Interface): |
969 | + """Marker interface for Session Storm database classes and instances.""" |
970 | + pass |
971 | |
972 | === added file 'lib/lp/services/session/model.py' |
973 | --- lib/lp/services/session/model.py 1970-01-01 00:00:00 +0000 |
974 | +++ lib/lp/services/session/model.py 2011-03-07 15:37:44 +0000 |
975 | @@ -0,0 +1,47 @@ |
976 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
977 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
978 | + |
979 | +"""Session Storm database classes""" |
980 | + |
981 | +__metaclass__ = type |
982 | +__all__ = ['SessionData', 'SessionPkgData'] |
983 | + |
984 | +from storm.locals import ( |
985 | + Pickle, |
986 | + Storm, |
987 | + Unicode, |
988 | + ) |
989 | +from zope.interface import ( |
990 | + classProvides, |
991 | + implements, |
992 | + ) |
993 | + |
994 | +from canonical.database.datetimecol import UtcDateTimeCol |
995 | +from lp.services.session.interfaces import IUseSessionStore |
996 | + |
997 | + |
998 | +class SessionData(Storm): |
999 | + """A user's Session.""" |
1000 | + |
1001 | + classProvides(IUseSessionStore) |
1002 | + implements(IUseSessionStore) |
1003 | + |
1004 | + __storm_table__ = 'SessionData' |
1005 | + client_id = Unicode(primary=True) |
1006 | + created = UtcDateTimeCol() |
1007 | + last_accessed = UtcDateTimeCol() |
1008 | + |
1009 | + |
1010 | +class SessionPkgData(Storm): |
1011 | + """Data storage for a Session.""" |
1012 | + |
1013 | + classProvides(IUseSessionStore) |
1014 | + implements(IUseSessionStore) |
1015 | + |
1016 | + __storm_table__ = 'SessionPkgData' |
1017 | + __storm_primary__ = 'client_id', 'product_id', 'key' |
1018 | + |
1019 | + client_id = Unicode() |
1020 | + product_id = Unicode() |
1021 | + key = Unicode() |
1022 | + pickle = Pickle() |
1023 | |
1024 | === added directory 'lib/lp/services/session/tests' |
1025 | === added file 'lib/lp/services/session/tests/__init__.py' |
1026 | === added file 'lib/lp/services/session/tests/test_session.py' |
1027 | --- lib/lp/services/session/tests/test_session.py 1970-01-01 00:00:00 +0000 |
1028 | +++ lib/lp/services/session/tests/test_session.py 2011-03-07 15:37:44 +0000 |
1029 | @@ -0,0 +1,32 @@ |
1030 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
1031 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
1032 | + |
1033 | +"""Session tests.""" |
1034 | + |
1035 | +__metaclass__ = type |
1036 | + |
1037 | +from canonical.launchpad.interfaces.lpstorm import ( |
1038 | + IMasterStore, |
1039 | + ISlaveStore, |
1040 | + IStore, |
1041 | + ) |
1042 | +from canonical.testing.layers import DatabaseFunctionalLayer |
1043 | +from lp.services.session.model import ( |
1044 | + SessionData, |
1045 | + SessionPkgData, |
1046 | + ) |
1047 | +from lp.testing import TestCase |
1048 | + |
1049 | + |
1050 | +class TestSessionModelAdapters(TestCase): |
1051 | + layer = DatabaseFunctionalLayer |
1052 | + |
1053 | + def test_adapters(self): |
1054 | + for adapter in [IMasterStore, ISlaveStore, IStore]: |
1055 | + for cls in [SessionData, SessionPkgData]: |
1056 | + for obj in [cls, cls()]: |
1057 | + store = adapter(obj) |
1058 | + self.assert_( |
1059 | + 'session' in store.get_database()._dsn, |
1060 | + 'Unknown store returned adapting %r to %r' |
1061 | + % (obj, adapter)) |
1062 | |
1063 | === modified file 'lib/lp/testing/factory.py' |
1064 | --- lib/lp/testing/factory.py 2011-03-06 23:15:05 +0000 |
1065 | +++ lib/lp/testing/factory.py 2011-03-07 15:37:44 +0000 |
1066 | @@ -101,6 +101,7 @@ |
1067 | ) |
1068 | from canonical.launchpad.webapp.sorting import sorted_version_numbers |
1069 | from lp.app.enums import ServiceUsage |
1070 | +from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet |
1071 | from lp.archiveuploader.dscfile import DSCFile |
1072 | from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy |
1073 | from lp.blueprints.enums import ( |
1074 | @@ -3850,6 +3851,20 @@ |
1075 | description = self.getUniqueString() |
1076 | return getUtility(ICveSet).new(sequence, description, cvestate) |
1077 | |
1078 | + def makePublisherConfig(self, distribution=None, root_dir=None, |
1079 | + base_url=None, copy_base_url=None): |
1080 | + """Create a new `PublisherConfig` record.""" |
1081 | + if distribution is None: |
1082 | + distribution = self.makeDistribution() |
1083 | + if root_dir is None: |
1084 | + root_dir = self.getUniqueString() |
1085 | + if base_url is None: |
1086 | + base_url = self.getUniqueString() |
1087 | + if copy_base_url is None: |
1088 | + copy_base_url = self.getUniqueString() |
1089 | + return getUtility(IPublisherConfigSet).new( |
1090 | + distribution, root_dir, base_url, copy_base_url) |
1091 | + |
1092 | |
1093 | # Some factory methods return simple Python types. We don't add |
1094 | # security wrappers for them, as well as for objects created by |
1095 | |
1096 | === modified file 'lib/lp/testing/tests/test_standard_test_template.py' |
1097 | --- lib/lp/testing/tests/test_standard_test_template.py 2011-02-02 13:19:02 +0000 |
1098 | +++ lib/lp/testing/tests/test_standard_test_template.py 2011-03-07 15:37:44 +0000 |
1099 | @@ -6,15 +6,16 @@ |
1100 | __metaclass__ = type |
1101 | |
1102 | from canonical.testing.layers import DatabaseFunctionalLayer |
1103 | -from lp.testing import TestCase # or TestCaseWithFactory |
1104 | +# or TestCaseWithFactory |
1105 | +from lp.testing import TestCase |
1106 | |
1107 | |
1108 | class TestSomething(TestCase): |
1109 | # XXX: Sample test class. Replace with your own test class(es). |
1110 | |
1111 | - # XXX: Optional layer--see lib/canonical/testing/layers.py |
1112 | - # Get the simplest layer that your test will work on, or if you |
1113 | - # don't even use the database, don't set it at all. |
1114 | + # XXX: layer--see lib/canonical/testing/layers.py |
1115 | + # Get the simplest layer that your test will work on. For unit tests |
1116 | + # requiring no resources, this is BaseLayer. |
1117 | layer = DatabaseFunctionalLayer |
1118 | |
1119 | # XXX: Sample test. Replace with your own test methods. |
This looks good.
The only thing I thought you might want to know was that the imports in archivepublishe r/model/ publisherconfig .py aren't
this section of lib/lp/
sorted:
from storm.locals import (
Int,
Reference,
Storm,
RawStr,
)