Merge lp:~julian-edwards/launchpad/publisher-config-db-schema into lp:launchpad

Proposed by Julian Edwards
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
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

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_publisherconfig

== Demo and Q/A ==
n/a yet

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This looks good.

The only thing I thought you might want to know was that the imports in
this section of lib/lp/archivepublisher/model/publisherconfig.py aren't
sorted:

    from storm.locals import (
        Int,
        Reference,
        Storm,
        RawStr,
        )

review: Approve (code)
Revision history for this message
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!

review: Needs Resubmitting

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'database/schema/launchpad_session.sql'
--- database/schema/launchpad_session.sql 2010-09-10 09:45:45 +0000
+++ database/schema/launchpad_session.sql 2011-03-07 15:37:44 +0000
@@ -29,3 +29,28 @@
29GRANT SELECT, INSERT, UPDATE, DELETE ON TimeLimitedToken TO session;29GRANT SELECT, INSERT, UPDATE, DELETE ON TimeLimitedToken TO session;
30-- And the garbo needs to run on it too.30-- And the garbo needs to run on it too.
31GRANT SELECT, DELETE ON TimeLimitedToken TO session;31GRANT SELECT, DELETE ON TimeLimitedToken TO session;
32
33
34-- This helper needs to exist in the session database so the BulkPruner
35-- can clean up unwanted sessions.
36CREATE OR REPLACE FUNCTION cursor_fetch(cur refcursor, n integer)
37RETURNS SETOF record LANGUAGE plpgsql AS
38$$
39DECLARE
40 r record;
41 count integer;
42BEGIN
43 FOR count IN 1..n LOOP
44 FETCH FORWARD FROM cur INTO r;
45 IF NOT FOUND THEN
46 RETURN;
47 END IF;
48 RETURN NEXT r;
49 END LOOP;
50END;
51$$;
52
53COMMENT ON FUNCTION cursor_fetch(refcursor, integer) IS
54'Fetch the next n items from a cursor. Work around for not being able to use FETCH inside a SELECT statement.';
55
56GRANT EXECUTE ON FUNCTION cursor_fetch(refcursor, integer) TO session;
3257
=== added file 'database/schema/patch-2208-99-0.sql'
--- database/schema/patch-2208-99-0.sql 1970-01-01 00:00:00 +0000
+++ database/schema/patch-2208-99-0.sql 2011-03-07 15:37:44 +0000
@@ -0,0 +1,14 @@
1SET client_min_messages=ERROR;
2
3CREATE TABLE PublisherConfig (
4 id serial PRIMARY KEY,
5 distribution integer NOT NULL CONSTRAINT publisherconfig__distribution__fk REFERENCES distribution,
6 root_dir text NOT NULL,
7 base_url text NOT NULL,
8 copy_base_url text NOT NULL
9);
10
11CREATE UNIQUE INDEX publisherconfig__distribution__idx
12 ON PublisherConfig(distribution);
13
14INSERT INTO LaunchpadDatabaseRevision VALUES (2208, 99, 0);
015
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2011-03-07 07:27:56 +0000
+++ database/schema/security.cfg 2011-03-07 15:37:44 +0000
@@ -260,6 +260,7 @@
260public.productrelease = SELECT, INSERT, UPDATE, DELETE260public.productrelease = SELECT, INSERT, UPDATE, DELETE
261public.productreleasefile = SELECT, INSERT, DELETE261public.productreleasefile = SELECT, INSERT, DELETE
262public.productseriescodeimport = SELECT, INSERT, UPDATE262public.productseriescodeimport = SELECT, INSERT, UPDATE
263public.publisherconfig = SELECT, INSERT, UPDATE, DELETE
263public.project = SELECT264public.project = SELECT
264public.projectbounty = SELECT, INSERT, UPDATE265public.projectbounty = SELECT, INSERT, UPDATE
265public.questionbug = SELECT, INSERT, DELETE266public.questionbug = SELECT, INSERT, DELETE
266267
=== modified file 'lib/lp/archivepublisher/config.py'
--- lib/lp/archivepublisher/config.py 2010-10-17 13:35:20 +0000
+++ lib/lp/archivepublisher/config.py 2011-03-07 15:37:44 +0000
@@ -83,10 +83,6 @@
83 return pubconf83 return pubconf
8484
8585
86class LucilleConfigError(Exception):
87 """Lucille configuration was not present."""
88
89
90class Config(object):86class Config(object):
91 """Manage a publisher configuration from the database. (Read Only)87 """Manage a publisher configuration from the database. (Read Only)
92 This class provides a useful abstraction so that if we change88 This class provides a useful abstraction so that if we change
9389
=== modified file 'lib/lp/archivepublisher/deathrow.py'
--- lib/lp/archivepublisher/deathrow.py 2010-10-17 13:35:20 +0000
+++ lib/lp/archivepublisher/deathrow.py 2011-03-07 15:37:44 +0000
@@ -17,7 +17,6 @@
17from lp.archivepublisher import ELIGIBLE_DOMINATION_STATES17from lp.archivepublisher import ELIGIBLE_DOMINATION_STATES
18from lp.archivepublisher.config import (18from lp.archivepublisher.config import (
19 getPubConfig,19 getPubConfig,
20 LucilleConfigError,
21 )20 )
22from lp.archivepublisher.diskpool import DiskPool21from lp.archivepublisher.diskpool import DiskPool
23from lp.archivepublisher.utils import process_in_batches22from lp.archivepublisher.utils import process_in_batches
@@ -40,12 +39,8 @@
40 the one provided by the publishing-configuration, it will be only39 the one provided by the publishing-configuration, it will be only
41 used for PRIMARY archives.40 used for PRIMARY archives.
42 """41 """
43 log.debug("Grab Lucille config.")42 log.debug("Grab publisher config.")
44 try:43 pubconf = getPubConfig(archive)
45 pubconf = getPubConfig(archive)
46 except LucilleConfigError, info:
47 log.error(info)
48 raise
4944
50 if (pool_root_override is not None and45 if (pool_root_override is not None and
51 archive.purpose == ArchivePurpose.PRIMARY):46 archive.purpose == ArchivePurpose.PRIMARY):
5247
=== added file 'lib/lp/archivepublisher/interfaces/publisherconfig.py'
--- lib/lp/archivepublisher/interfaces/publisherconfig.py 1970-01-01 00:00:00 +0000
+++ lib/lp/archivepublisher/interfaces/publisherconfig.py 2011-03-07 15:37:44 +0000
@@ -0,0 +1,58 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4# pylint: disable-msg=E0211,E0213
5
6"""PublisherConfig interface."""
7
8__metaclass__ = type
9
10__all__ = [
11 'IPublisherConfig',
12 'IPublisherConfigSet',
13 ]
14
15from lazr.restful.fields import Reference
16from zope.interface import Interface
17from zope.schema import (
18 Int,
19 TextLine,
20 )
21
22from canonical.launchpad import _
23from lp.registry.interfaces.distribution import IDistribution
24
25
26class IPublisherConfig(Interface):
27 """`PublisherConfig` interface."""
28
29 id = Int(title=_('ID'), required=True, readonly=True)
30
31 distribution = Reference(
32 IDistribution, title=_("Distribution"), required=True,
33 description=_("The Distribution for this configuration."))
34
35 root_dir = TextLine(
36 title=_("Root Directory"), required=True,
37 description=_("The root directory for published archives."))
38
39 base_url = TextLine(
40 title=_("Base URL"), required=True,
41 description=_("The base URL for published archives"))
42
43 copy_base_url = TextLine(
44 title=_("Copy Base URL"), required=True,
45 description=_("The base URL for published copy archives"))
46
47
48class IPublisherConfigSet(Interface):
49 """`PublisherConfigSet` interface."""
50
51 def new(distribution, root_dir, base_url, copy_base_url):
52 """Create a new `PublisherConfig`."""
53
54 def getByDistribution(distribution):
55 """Get the config for a a distribution.
56
57 :param distribution: An `IDistribution`
58 """
059
=== added file 'lib/lp/archivepublisher/model/publisherconfig.py'
--- lib/lp/archivepublisher/model/publisherconfig.py 1970-01-01 00:00:00 +0000
+++ lib/lp/archivepublisher/model/publisherconfig.py 2011-03-07 15:37:44 +0000
@@ -0,0 +1,68 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Database class for table PublisherConfig."""
5
6__metaclass__ = type
7
8__all__ = [
9 'PublisherConfig',
10 'PublisherConfigSet',
11 ]
12
13from storm.locals import (
14 Int,
15 RawStr,
16 Reference,
17 Storm,
18 )
19from zope.interface import implements
20
21from canonical.launchpad.interfaces.lpstorm import (
22 IMasterStore,
23 )
24from lp.archivepublisher.interfaces.publisherconfig import (
25 IPublisherConfig,
26 IPublisherConfigSet,
27 )
28
29
30class PublisherConfig(Storm):
31 """See `IArchiveAuthToken`."""
32 implements(IPublisherConfig)
33 __storm_table__ = 'PublisherConfig'
34
35 id = Int(primary=True)
36
37 distribution_id = Int(name='distribution', allow_none=False)
38 distribution = Reference(distribution_id, 'Distribution.id')
39
40 root_dir = RawStr(name='root_dir', allow_none=False)
41
42 base_url = RawStr(name='base_url', allow_none=False)
43
44 copy_base_url = RawStr(name='copy_base_url', allow_none=False)
45
46
47class PublisherConfigSet:
48 """See `IPublisherConfigSet`."""
49 implements(IPublisherConfigSet)
50 title = "Soyuz Publisher Configurations"
51
52 def new(self, distribution, root_dir, base_url, copy_base_url):
53 """Make and return a new `PublisherConfig`."""
54 store = IMasterStore(PublisherConfig)
55 pubconf = PublisherConfig()
56 pubconf.distribution = distribution
57 pubconf.root_dir = root_dir
58 pubconf.base_url = base_url
59 pubconf.copy_base_url = copy_base_url
60 store.add(pubconf)
61 return pubconf
62
63 def getByDistribution(self, distribution):
64 """See `IArchiveAuthTokenSet`."""
65 store = IMasterStore(PublisherConfig)
66 return store.find(
67 PublisherConfig,
68 PublisherConfig.distribution_id == distribution.id).one()
069
=== modified file 'lib/lp/archivepublisher/publishing.py'
--- lib/lp/archivepublisher/publishing.py 2011-02-04 09:07:36 +0000
+++ lib/lp/archivepublisher/publishing.py 2011-03-07 15:37:44 +0000
@@ -21,7 +21,6 @@
21from lp.archivepublisher import HARDCODED_COMPONENT_ORDER21from lp.archivepublisher import HARDCODED_COMPONENT_ORDER
22from lp.archivepublisher.config import (22from lp.archivepublisher.config import (
23 getPubConfig,23 getPubConfig,
24 LucilleConfigError,
25 )24 )
26from lp.archivepublisher.diskpool import DiskPool25from lp.archivepublisher.diskpool import DiskPool
27from lp.archivepublisher.domination import Dominator26from lp.archivepublisher.domination import Dominator
@@ -120,11 +119,7 @@
120 else:119 else:
121 log.debug("Finding configuration for '%s' PPA."120 log.debug("Finding configuration for '%s' PPA."
122 % archive.owner.name)121 % archive.owner.name)
123 try:122 pubconf = getPubConfig(archive)
124 pubconf = getPubConfig(archive)
125 except LucilleConfigError, info:
126 log.error(info)
127 raise
128123
129 disk_pool = _getDiskPool(pubconf, log)124 disk_pool = _getDiskPool(pubconf, log)
130125
131126
=== added file 'lib/lp/archivepublisher/tests/test_publisherconfig.py'
--- lib/lp/archivepublisher/tests/test_publisherconfig.py 1970-01-01 00:00:00 +0000
+++ lib/lp/archivepublisher/tests/test_publisherconfig.py 2011-03-07 15:37:44 +0000
@@ -0,0 +1,66 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for publisherConfig model class."""
5
6__metaclass__ = type
7
8
9from storm.store import Store
10from storm.exceptions import IntegrityError
11from zope.component import getUtility
12from zope.interface.verify import verifyObject
13
14from canonical.testing.layers import ZopelessDatabaseLayer
15from lp.archivepublisher.interfaces.publisherconfig import (
16 IPublisherConfig,
17 IPublisherConfigSet,
18 )
19from lp.testing import TestCaseWithFactory
20
21
22class TestPublisherConfig(TestCaseWithFactory):
23 """Test the `PublisherConfig` model."""
24 layer = ZopelessDatabaseLayer
25
26 def setUp(self):
27 TestCaseWithFactory.setUp(self)
28 self.distribution = self.factory.makeDistribution(name='conftest')
29
30 def test_verify_interface(self):
31 # Test the interface for the model.
32 pubconf = self.factory.makePublisherConfig()
33 verified = verifyObject(IPublisherConfig, pubconf)
34 self.assertTrue(verified)
35
36 def test_properties(self):
37 # Test the model properties.
38 ROOT_DIR = "rootdir/test"
39 BASE_URL = "http://base.url"
40 COPY_BASE_URL = "http://base.url"
41 pubconf = self.factory.makePublisherConfig(
42 distribution=self.distribution,
43 root_dir=ROOT_DIR,
44 base_url=BASE_URL,
45 copy_base_url=COPY_BASE_URL,
46 )
47
48 self.assertEqual(self.distribution.name, pubconf.distribution.name)
49 self.assertEqual(ROOT_DIR, pubconf.root_dir)
50 self.assertEqual(BASE_URL, pubconf.base_url)
51 self.assertEqual(COPY_BASE_URL, pubconf.copy_base_url)
52
53 def test_one_config_per_distro(self):
54 # Only one config for each distro is allowed.
55 pubconf = self.factory.makePublisherConfig(self.distribution)
56 pubconf2 = self.factory.makePublisherConfig(self.distribution)
57 store = Store.of(pubconf)
58 self.assertRaises(IntegrityError, store.flush)
59
60 def test_getByDistribution(self):
61 # Test that IPublisherConfigSet.getByDistribution works.
62 pubconf = self.factory.makePublisherConfig(
63 distribution=self.distribution)
64 pubconf = getUtility(IPublisherConfigSet).getByDistribution(
65 self.distribution)
66 self.assertEqual(self.distribution.name, pubconf.distribution.name)
067
=== modified file 'lib/lp/archivepublisher/zcml/configure.zcml'
--- lib/lp/archivepublisher/zcml/configure.zcml 2009-07-13 18:15:02 +0000
+++ lib/lp/archivepublisher/zcml/configure.zcml 2011-03-07 15:37:44 +0000
@@ -2,8 +2,30 @@
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
5<configure xmlns="http://namespaces.zope.org/zope">5<configure
6 xmlns="http://namespaces.zope.org/zope"
7 xmlns:browser="http://namespaces.zope.org/browser"
8 xmlns:i18n="http://namespaces.zope.org/i18n"
9 xmlns:webservice="http://namespaces.canonical.com/webservice"
10 xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"
11 i18n_domain="launchpad">
12
6 <include package="lp.archivepublisher.zcml"13 <include package="lp.archivepublisher.zcml"
7 file="archivesigningkey.zcml" />14 file="archivesigningkey.zcml" />
15
16 <securedutility
17 class="lp.archivepublisher.model.publisherconfig.PublisherConfigSet"
18 provides="lp.archivepublisher.interfaces.publisherconfig.IPublisherConfigSet">
19 <allow
20 interface="lp.archivepublisher.interfaces.publisherconfig.IPublisherConfigSet"/>
21 </securedutility>
22
23 <class
24 class="lp.archivepublisher.model.publisherconfig.PublisherConfig">
25 <allow
26 interface="lp.archivepublisher.interfaces.publisherconfig.IPublisherConfig" />
27 </class>
28
29
8</configure>30</configure>
931
1032
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2011-03-02 23:08:54 +0000
+++ lib/lp/bugs/configure.zcml 2011-03-07 15:37:44 +0000
@@ -1056,6 +1056,10 @@
1056 class="lp.bugs.mail.bugnotificationrecipients.BugNotificationRecipients">1056 class="lp.bugs.mail.bugnotificationrecipients.BugNotificationRecipients">
1057 <allow1057 <allow
1058 interface="canonical.launchpad.interfaces.launchpad.INotificationRecipientSet"/>1058 interface="canonical.launchpad.interfaces.launchpad.INotificationRecipientSet"/>
1059 <!-- BugNotificationRecipients provides the following
1060 attributes/methods in addition. -->
1061 <allow
1062 attributes="subscription_filters addFilter"/>
1059 </class>1063 </class>
1060 <securedutility1064 <securedutility
1061 provides="lp.bugs.interfaces.bugnotification.IBugNotificationSet"1065 provides="lp.bugs.interfaces.bugnotification.IBugNotificationSet"
10621066
=== modified file 'lib/lp/bugs/doc/bugnotification-sending.txt'
--- lib/lp/bugs/doc/bugnotification-sending.txt 2011-02-22 10:44:48 +0000
+++ lib/lp/bugs/doc/bugnotification-sending.txt 2011-03-07 15:37:44 +0000
@@ -21,8 +21,10 @@
2121
22 >>> def print_notification_headers(email_notification):22 >>> def print_notification_headers(email_notification):
23 ... for header in ['To', 'From', 'Subject',23 ... for header in ['To', 'From', 'Subject',
24 ... 'X-Launchpad-Message-Rationale']:24 ... 'X-Launchpad-Message-Rationale',
25 ... print "%s: %s" % (header, email_notification[header])25 ... 'X-Launchpad-Subscription-Filter']:
26 ... if email_notification[header]:
27 ... print "%s: %s" % (header, email_notification[header])
2628
27 >>> def print_notification(email_notification):29 >>> def print_notification(email_notification):
28 ... print_notification_headers(email_notification)30 ... print_notification_headers(email_notification)
@@ -676,7 +678,6 @@
676been set properly.678been set properly.
677679
678 >>> from lp.bugs.model.bugnotification import BugNotification680 >>> from lp.bugs.model.bugnotification import BugNotification
679 >>> from lp.bugs.enum import BugNotificationStatus
680 >>> for notification in BugNotification.select(orderBy='id')[-8:]:681 >>> for notification in BugNotification.select(orderBy='id')[-8:]:
681 ... if notification.is_comment:682 ... if notification.is_comment:
682 ... identifier = 'comment'683 ... identifier = 'comment'
@@ -1247,6 +1248,7 @@
1247 >>> with lp_dbuser():1248 >>> with lp_dbuser():
1248 ... filter = subscription_no_priv.newBugFilter()1249 ... filter = subscription_no_priv.newBugFilter()
1249 ... filter.bug_notification_level = BugNotificationLevel.COMMENTS1250 ... filter.bug_notification_level = BugNotificationLevel.COMMENTS
1251 ... filter.description = u"Allow-comments filter"
12501252
1251 >>> comment = getUtility(IMessageSet).fromText(1253 >>> comment = getUtility(IMessageSet).fromText(
1252 ... 'subject', 'another comment.', sample_person,1254 ... 'subject', 'another comment.', sample_person,
@@ -1279,12 +1281,14 @@
1279 From: Sample Person <...@bugs.launchpad.net>1281 From: Sample Person <...@bugs.launchpad.net>
1280 Subject: [Bug 1] subject1282 Subject: [Bug 1] subject
1281 X-Launchpad-Message-Rationale: Subscriber (Mozilla Firefox)1283 X-Launchpad-Message-Rationale: Subscriber (Mozilla Firefox)
1284 X-Launchpad-Subscription-Filter: Allow-comments filter
1282 <BLANKLINE>1285 <BLANKLINE>
1283 another comment.1286 another comment.
1284 <BLANKLINE>1287 <BLANKLINE>
1285 --1288 --
1286 You received this bug notification because you are subscribed to Mozilla1289 You received this bug notification because you are subscribed to Mozilla
1287 Firefox.1290 Firefox.
1291 Matching filters: Allow-comments filter
1288 ...1292 ...
1289 ----------------------------------------------------------------------1293 ----------------------------------------------------------------------
1290 To: support@ubuntu.com1294 To: support@ubuntu.com
@@ -1390,6 +1394,7 @@
1390 From: Sample Person <...@bugs.launchpad.net>1394 From: Sample Person <...@bugs.launchpad.net>
1391 Subject: [Bug 1] subject1395 Subject: [Bug 1] subject
1392 X-Launchpad-Message-Rationale: Subscriber (Mozilla Firefox)1396 X-Launchpad-Message-Rationale: Subscriber (Mozilla Firefox)
1397 X-Launchpad-Subscription-Filter: Allow-comments filter
1393 <BLANKLINE>1398 <BLANKLINE>
1394 no comment for no-priv.1399 no comment for no-priv.
1395 <BLANKLINE>1400 <BLANKLINE>
@@ -1400,6 +1405,7 @@
1400 --1405 --
1401 You received this bug notification because you are subscribed to Mozilla1406 You received this bug notification because you are subscribed to Mozilla
1402 Firefox.1407 Firefox.
1408 Matching filters: Allow-comments filter
1403 ...1409 ...
1404 ----------------------------------------------------------------------1410 ----------------------------------------------------------------------
1405 To: support@ubuntu.com1411 To: support@ubuntu.com
14061412
=== modified file 'lib/lp/bugs/enum.py'
--- lib/lp/bugs/enum.py 2011-03-05 00:06:26 +0000
+++ lib/lp/bugs/enum.py 2011-03-07 15:37:44 +0000
@@ -57,18 +57,18 @@
5757
58class BugNotificationStatus(DBEnumeratedType):58class BugNotificationStatus(DBEnumeratedType):
59 """The status of a bug notification.59 """The status of a bug notification.
60 60
61 A notification may be pending, sent, or omitted."""61 A notification may be pending, sent, or omitted."""
6262
63 PENDING = DBItem(10, """63 PENDING = DBItem(10, """
64 Pending64 Pending
65 65
66 The notification has not yet been sent.66 The notification has not yet been sent.
67 """)67 """)
6868
69 OMITTED = DBItem(20, """69 OMITTED = DBItem(20, """
70 Omitted70 Omitted
71 71
72 The system considered sending the notification, but omitted it.72 The system considered sending the notification, but omitted it.
73 This is generally because the action reported by the notification73 This is generally because the action reported by the notification
74 was immediately undone.74 was immediately undone.
@@ -76,6 +76,6 @@
7676
77 SENT = DBItem(30, """77 SENT = DBItem(30, """
78 Sent78 Sent
79 79
80 The notification has been sent.80 The notification has been sent.
81 """)81 """)
8282
=== modified file 'lib/lp/bugs/mail/bugnotificationrecipients.py'
--- lib/lp/bugs/mail/bugnotificationrecipients.py 2011-03-02 14:07:23 +0000
+++ lib/lp/bugs/mail/bugnotificationrecipients.py 2011-03-07 15:37:44 +0000
@@ -58,6 +58,7 @@
58 """58 """
59 NotificationRecipientSet.__init__(self)59 NotificationRecipientSet.__init__(self)
60 self.duplicateof = duplicateof60 self.duplicateof = duplicateof
61 self.subscription_filters = set()
6162
62 def _addReason(self, person, reason, header):63 def _addReason(self, person, reason, header):
63 """Adds a reason (text and header) for a person.64 """Adds a reason (text and header) for a person.
@@ -127,3 +128,13 @@
127 else:128 else:
128 text = "are the registrant for %s" % upstream.displayname129 text = "are the registrant for %s" % upstream.displayname
129 self._addReason(person, text, reason)130 self._addReason(person, text, reason)
131
132 def update(self, recipient_set):
133 """See `INotificationRecipientSet`."""
134 super(BugNotificationRecipients, self).update(recipient_set)
135 self.subscription_filters.update(
136 recipient_set.subscription_filters)
137
138 def addFilter(self, subscription_filter):
139 if subscription_filter is not None:
140 self.subscription_filters.add(subscription_filter)
130141
=== modified file 'lib/lp/bugs/model/bugnotification.py'
--- lib/lp/bugs/model/bugnotification.py 2011-02-25 16:19:58 +0000
+++ lib/lp/bugs/model/bugnotification.py 2011-03-07 15:37:44 +0000
@@ -150,12 +150,23 @@
150 reason_body, reason_header = recipients.getReason(recipient)150 reason_body, reason_header = recipients.getReason(recipient)
151 sql_values.append('(%s, %s, %s, %s)' % sqlvalues(151 sql_values.append('(%s, %s, %s, %s)' % sqlvalues(
152 bug_notification, recipient, reason_header, reason_body))152 bug_notification, recipient, reason_header, reason_body))
153
153 # We add all the recipients in a single SQL statement to make154 # We add all the recipients in a single SQL statement to make
154 # this a bit more efficient for bugs with many subscribers.155 # this a bit more efficient for bugs with many subscribers.
155 store.execute("""156 store.execute("""
156 INSERT INTO BugNotificationRecipient157 INSERT INTO BugNotificationRecipient
157 (bug_notification, person, reason_header, reason_body)158 (bug_notification, person, reason_header, reason_body)
158 VALUES %s;""" % ', '.join(sql_values))159 VALUES %s;""" % ', '.join(sql_values))
160
161 if len(recipients.subscription_filters) > 0:
162 filter_link_sql = [
163 "(%s, %s)" % sqlvalues(bug_notification, filter.id)
164 for filter in recipients.subscription_filters]
165 store.execute("""
166 INSERT INTO BugNotificationFilter
167 (bug_notification, bug_subscription_filter)
168 VALUES %s;""" % ", ".join(filter_link_sql))
169
159 return bug_notification170 return bug_notification
160171
161172
162173
=== modified file 'lib/lp/bugs/model/structuralsubscription.py'
--- lib/lp/bugs/model/structuralsubscription.py 2011-03-04 02:29:09 +0000
+++ lib/lp/bugs/model/structuralsubscription.py 2011-03-07 15:37:44 +0000
@@ -585,14 +585,15 @@
585 else:585 else:
586 subscribers = []586 subscribers = []
587 query_results = source.find(587 query_results = source.find(
588 (Person, StructuralSubscription),588 (Person, StructuralSubscription, BugSubscriptionFilter),
589 *constraints).config(distinct=True)589 *constraints)
590 for person, subscription in query_results:590 for person, subscription, filter in query_results:
591 # Set up results.591 # Set up results.
592 if person not in recipients:592 if person not in recipients:
593 subscribers.append(person)593 subscribers.append(person)
594 recipients.addStructuralSubscriber(594 recipients.addStructuralSubscriber(
595 person, subscription.target)595 person, subscription.target)
596 recipients.addFilter(filter)
596 return subscribers597 return subscribers
597598
598599
599600
=== modified file 'lib/lp/scripts/garbo.py'
--- lib/lp/scripts/garbo.py 2011-02-23 10:28:53 +0000
+++ lib/lp/scripts/garbo.py 2011-03-07 15:37:44 +0000
@@ -74,6 +74,7 @@
74 LaunchpadCronScript,74 LaunchpadCronScript,
75 SilentLaunchpadScriptFailure,75 SilentLaunchpadScriptFailure,
76 )76 )
77from lp.services.session.model import SessionData
77from lp.translations.interfaces.potemplate import IPOTemplateSet78from lp.translations.interfaces.potemplate import IPOTemplateSet
78from lp.translations.model.potranslation import POTranslation79from lp.translations.model.potranslation import POTranslation
7980
@@ -107,10 +108,14 @@
107 # from. Must be overridden.108 # from. Must be overridden.
108 target_table_class = None109 target_table_class = None
109110
110 # The column name in target_table we use as the integer key. May be111 # The column name in target_table we use as the key. The type must
111 # overridden.112 # match that returned by the ids_to_prune_query and the
113 # target_table_key_type. May be overridden.
112 target_table_key = 'id'114 target_table_key = 'id'
113115
116 # SQL type of the target_table_key. May be overridden.
117 target_table_key_type = 'integer'
118
114 # An SQL query returning a list of ids to remove from target_table.119 # An SQL query returning a list of ids to remove from target_table.
115 # The query must return a single column named 'id' and should not120 # The query must return a single column named 'id' and should not
116 # contain duplicates. Must be overridden.121 # contain duplicates. Must be overridden.
@@ -119,10 +124,23 @@
119 # See `TunableLoop`. May be overridden.124 # See `TunableLoop`. May be overridden.
120 maximum_chunk_size = 10000125 maximum_chunk_size = 10000
121126
127 # Optional extra WHERE clause fragment for the deletion to skip
128 # arbitrary rows flagged for deletion. For example, skip rows
129 # that might have been modified since the set of ids_to_prune
130 # was calculated.
131 extra_prune_clause = None
132
133 def getStore(self):
134 """The master Store for the table we are pruning.
135
136 May be overridden.
137 """
138 return IMasterStore(self.target_table_class)
139
122 def __init__(self, log, abort_time=None):140 def __init__(self, log, abort_time=None):
123 super(BulkPruner, self).__init__(log, abort_time)141 super(BulkPruner, self).__init__(log, abort_time)
124142
125 self.store = IMasterStore(self.target_table_class)143 self.store = self.getStore()
126 self.target_table_name = self.target_table_class.__storm_table__144 self.target_table_name = self.target_table_class.__storm_table__
127145
128 # Open the cursor.146 # Open the cursor.
@@ -138,12 +156,20 @@
138156
139 def __call__(self, chunk_size):157 def __call__(self, chunk_size):
140 """See `ITunableLoop`."""158 """See `ITunableLoop`."""
159 if self.extra_prune_clause:
160 extra = "AND (%s)" % self.extra_prune_clause
161 else:
162 extra = ""
141 result = self.store.execute("""163 result = self.store.execute("""
142 DELETE FROM %s WHERE %s IN (164 DELETE FROM %s
165 WHERE %s IN (
143 SELECT id FROM166 SELECT id FROM
144 cursor_fetch('bulkprunerid', %d) AS f(id integer))167 cursor_fetch('bulkprunerid', %d) AS f(id %s))
168 %s
145 """169 """
146 % (self.target_table_name, self.target_table_key, chunk_size))170 % (
171 self.target_table_name, self.target_table_key,
172 chunk_size, self.target_table_key_type, extra))
147 self._num_removed = result.rowcount173 self._num_removed = result.rowcount
148 transaction.commit()174 transaction.commit()
149175
@@ -157,9 +183,7 @@
157183
158 XXX bug=723596 StuartBishop: This job only needs to run once per month.184 XXX bug=723596 StuartBishop: This job only needs to run once per month.
159 """185 """
160
161 target_table_class = POTranslation186 target_table_class = POTranslation
162
163 ids_to_prune_query = """187 ids_to_prune_query = """
164 SELECT POTranslation.id AS id FROM POTranslation188 SELECT POTranslation.id AS id FROM POTranslation
165 EXCEPT (189 EXCEPT (
@@ -186,6 +210,45 @@
186 """210 """
187211
188212
213class SessionPruner(BulkPruner):
214 """Base class for session removal."""
215
216 target_table_class = SessionData
217 target_table_key = 'client_id'
218 target_table_key_type = 'text'
219
220
221class AntiqueSessionPruner(SessionPruner):
222 """Remove sessions not accessed for 60 days"""
223
224 ids_to_prune_query = """
225 SELECT client_id AS id FROM SessionData
226 WHERE last_accessed < CURRENT_TIMESTAMP - CAST('60 days' AS interval)
227 """
228
229
230class UnusedSessionPruner(SessionPruner):
231 """Remove sessions older than 1 day with no authentication credentials."""
232
233 ids_to_prune_query = """
234 SELECT client_id AS id FROM SessionData
235 WHERE
236 last_accessed < CURRENT_TIMESTAMP - CAST('1 day' AS interval)
237 AND client_id NOT IN (
238 SELECT client_id
239 FROM SessionPkgData
240 WHERE
241 product_id = 'launchpad.authenticateduser'
242 AND key='logintime')
243 """
244
245 # Don't delete a session if it has been used between calculating
246 # the list of sessions to remove and the current iteration.
247 prune_extra_clause = """
248 last_accessed < CURRENT_TIMESTAMP - CAST('1 day' AS interval)
249 """
250
251
189class OAuthNoncePruner(TunableLoop):252class OAuthNoncePruner(TunableLoop):
190 """An ITunableLoop to prune old OAuthNonce records.253 """An ITunableLoop to prune old OAuthNonce records.
191254
@@ -977,6 +1040,8 @@
977 RevisionCachePruner,1040 RevisionCachePruner,
978 BugHeatUpdater,1041 BugHeatUpdater,
979 BugWatchScheduler,1042 BugWatchScheduler,
1043 AntiqueSessionPruner,
1044 UnusedSessionPruner,
980 ]1045 ]
981 experimental_tunable_loops = []1046 experimental_tunable_loops = []
9821047
9831048
=== modified file 'lib/lp/scripts/tests/test_garbo.py'
--- lib/lp/scripts/tests/test_garbo.py 2011-02-28 11:50:34 +0000
+++ lib/lp/scripts/tests/test_garbo.py 2011-03-07 15:37:44 +0000
@@ -10,7 +10,6 @@
10 datetime,10 datetime,
11 timedelta,11 timedelta,
12 )12 )
13import operator
14import time13import time
1514
16from pytz import UTC15from pytz import UTC
@@ -18,7 +17,10 @@
18 Min,17 Min,
19 SQL,18 SQL,
20 )19 )
21from storm.locals import Storm, Int20from storm.locals import (
21 Int,
22 Storm,
23 )
22from storm.store import Store24from storm.store import Store
23import transaction25import transaction
24from zope.component import getUtility26from zope.component import getUtility
@@ -69,13 +71,19 @@
69 PersonCreationRationale,71 PersonCreationRationale,
70 )72 )
71from lp.scripts.garbo import (73from lp.scripts.garbo import (
74 AntiqueSessionPruner,
72 BulkPruner,75 BulkPruner,
73 DailyDatabaseGarbageCollector,76 DailyDatabaseGarbageCollector,
74 HourlyDatabaseGarbageCollector,77 HourlyDatabaseGarbageCollector,
75 OpenIDConsumerAssociationPruner,78 OpenIDConsumerAssociationPruner,
79 UnusedSessionPruner,
76 )80 )
77from lp.services.job.model.job import Job81from lp.services.job.model.job import Job
78from lp.services.log.logger import BufferLogger82from lp.services.log.logger import BufferLogger
83from lp.services.session.model import (
84 SessionData,
85 SessionPkgData,
86 )
79from lp.testing import (87from lp.testing import (
80 TestCase,88 TestCase,
81 TestCaseWithFactory,89 TestCaseWithFactory,
@@ -181,6 +189,97 @@
181 pruner(chunk_size)189 pruner(chunk_size)
182190
183191
192class TestSessionPruner(TestCase):
193 layer = ZopelessDatabaseLayer
194
195 def setUp(self):
196 super(TestCase, self).setUp()
197
198 # Session database isn't reset between tests. We need to do this
199 # manually.
200 nuke_all_sessions = IMasterStore(SessionData).find(SessionData).remove
201 nuke_all_sessions()
202 self.addCleanup(nuke_all_sessions)
203
204 recent = datetime.now(UTC)
205 yesterday = recent - timedelta(days=1)
206 ancient = recent - timedelta(days=61)
207
208 def make_session(client_id, accessed, authenticated=None):
209 session_data = SessionData()
210 session_data.client_id = client_id
211 session_data.last_accessed = accessed
212 IMasterStore(SessionData).add(session_data)
213
214 if authenticated:
215 session_pkg_data = SessionPkgData()
216 session_pkg_data.client_id = client_id
217 session_pkg_data.product_id = u'launchpad.authenticateduser'
218 session_pkg_data.key = u'logintime'
219 session_pkg_data.pickle = 'value is ignored'
220 IMasterStore(SessionPkgData).add(session_pkg_data)
221
222 make_session(u'recent_auth', recent, True)
223 make_session(u'recent_unauth', recent, False)
224 make_session(u'yesterday_auth', yesterday, True)
225 make_session(u'yesterday_unauth', yesterday, False)
226 make_session(u'ancient_auth', ancient, True)
227 make_session(u'ancient_unauth', ancient, False)
228
229 def sessionExists(self, client_id):
230 store = IMasterStore(SessionData)
231 return not store.find(
232 SessionData, SessionData.client_id == client_id).is_empty()
233
234 def test_antique_session_pruner(self):
235 chunk_size = 2
236 log = BufferLogger()
237 pruner = AntiqueSessionPruner(log)
238 try:
239 while not pruner.isDone():
240 pruner(chunk_size)
241 finally:
242 pruner.cleanUp()
243
244 expected_sessions = set([
245 u'recent_auth',
246 u'recent_unauth',
247 u'yesterday_auth',
248 u'yesterday_unauth',
249 # u'ancient_auth',
250 # u'ancient_unauth',
251 ])
252
253 found_sessions = set(
254 IMasterStore(SessionData).find(SessionData.client_id))
255
256 self.assertEqual(expected_sessions, found_sessions)
257
258 def test_unused_session_pruner(self):
259 chunk_size = 2
260 log = BufferLogger()
261 pruner = UnusedSessionPruner(log)
262 try:
263 while not pruner.isDone():
264 pruner(chunk_size)
265 finally:
266 pruner.cleanUp()
267
268 expected_sessions = set([
269 u'recent_auth',
270 u'recent_unauth',
271 u'yesterday_auth',
272 # u'yesterday_unauth',
273 u'ancient_auth',
274 # u'ancient_unauth',
275 ])
276
277 found_sessions = set(
278 IMasterStore(SessionData).find(SessionData.client_id))
279
280 self.assertEqual(expected_sessions, found_sessions)
281
282
184class TestGarbo(TestCaseWithFactory):283class TestGarbo(TestCaseWithFactory):
185 layer = LaunchpadZopelessLayer284 layer = LaunchpadZopelessLayer
186285
@@ -209,7 +308,7 @@
209 return collector308 return collector
210309
211 def test_OAuthNoncePruner(self):310 def test_OAuthNoncePruner(self):
212 now = datetime.utcnow().replace(tzinfo=UTC)311 now = datetime.now(UTC)
213 timestamps = [312 timestamps = [
214 now - timedelta(days=2), # Garbage313 now - timedelta(days=2), # Garbage
215 now - timedelta(days=1) - timedelta(seconds=60), # Garbage314 now - timedelta(days=1) - timedelta(seconds=60), # Garbage
@@ -287,7 +386,7 @@
287 self.failUnless(earliest >= now - 24*60*60, 'Still have old nonces')386 self.failUnless(earliest >= now - 24*60*60, 'Still have old nonces')
288387
289 def test_CodeImportResultPruner(self):388 def test_CodeImportResultPruner(self):
290 now = datetime.utcnow().replace(tzinfo=UTC)389 now = datetime.now(UTC)
291 store = IMasterStore(CodeImportResult)390 store = IMasterStore(CodeImportResult)
292391
293 results_to_keep_count = (392 results_to_keep_count = (
@@ -344,7 +443,7 @@
344 >= now - timedelta(days=30))443 >= now - timedelta(days=30))
345444
346 def test_CodeImportEventPruner(self):445 def test_CodeImportEventPruner(self):
347 now = datetime.utcnow().replace(tzinfo=UTC)446 now = datetime.now(UTC)
348 store = IMasterStore(CodeImportResult)447 store = IMasterStore(CodeImportResult)
349448
350 LaunchpadZopelessLayer.switchDbUser('testadmin')449 LaunchpadZopelessLayer.switchDbUser('testadmin')
351450
=== modified file 'lib/lp/services/configure.zcml'
--- lib/lp/services/configure.zcml 2011-03-04 17:05:09 +0000
+++ lib/lp/services/configure.zcml 2011-03-07 15:37:44 +0000
@@ -16,5 +16,6 @@
16 <include package=".salesforce" />16 <include package=".salesforce" />
17 <include package=".scripts" />17 <include package=".scripts" />
18 <include package=".search" />18 <include package=".search" />
19 <include package=".session" />
19 <include package=".worlddata" />20 <include package=".worlddata" />
20</configure>21</configure>
2122
=== added directory 'lib/lp/services/session'
=== added file 'lib/lp/services/session/__init__.py'
=== added file 'lib/lp/services/session/adapters.py'
--- lib/lp/services/session/adapters.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/session/adapters.py 2011-03-07 15:37:44 +0000
@@ -0,0 +1,40 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Session adapters."""
5
6__metaclass__ = type
7__all__ = []
8
9
10from zope.component import adapter
11from zope.interface import implementer
12
13from canonical.database.sqlbase import session_store
14from canonical.launchpad.interfaces.lpstorm import (
15 IMasterStore,
16 ISlaveStore,
17 IStore,
18 )
19from lp.services.session.interfaces import IUseSessionStore
20
21
22@adapter(IUseSessionStore)
23@implementer(IMasterStore)
24def session_master_store(cls):
25 """Adapt a Session database object to an `IMasterStore`."""
26 return session_store()
27
28
29@adapter(IUseSessionStore)
30@implementer(ISlaveStore)
31def session_slave_store(cls):
32 """Adapt a Session database object to an `ISlaveStore`."""
33 return session_store()
34
35
36@adapter(IUseSessionStore)
37@implementer(IStore)
38def session_default_store(cls):
39 """Adapt an Session database object to an `IStore`."""
40 return session_store()
041
=== added file 'lib/lp/services/session/configure.zcml'
--- lib/lp/services/session/configure.zcml 1970-01-01 00:00:00 +0000
+++ lib/lp/services/session/configure.zcml 2011-03-07 15:37:44 +0000
@@ -0,0 +1,12 @@
1<!-- Copyright 2011 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->
4<configure
5 xmlns="http://namespaces.zope.org/zope"
6 xmlns:browser="http://namespaces.zope.org/browser"
7 xmlns:i18n="http://namespaces.zope.org/i18n"
8 i18n_domain="launchpad">
9 <adapter factory=".adapters.session_master_store" />
10 <adapter factory=".adapters.session_slave_store" />
11 <adapter factory=".adapters.session_default_store" />
12</configure>
013
=== added file 'lib/lp/services/session/interfaces.py'
--- lib/lp/services/session/interfaces.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/session/interfaces.py 2011-03-07 15:37:44 +0000
@@ -0,0 +1,15 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Session interfaces."""
5
6__metaclass__ = type
7__all__ = ['IUseSessionStore']
8
9
10from zope.interface import Interface
11
12
13class IUseSessionStore(Interface):
14 """Marker interface for Session Storm database classes and instances."""
15 pass
016
=== added file 'lib/lp/services/session/model.py'
--- lib/lp/services/session/model.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/session/model.py 2011-03-07 15:37:44 +0000
@@ -0,0 +1,47 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Session Storm database classes"""
5
6__metaclass__ = type
7__all__ = ['SessionData', 'SessionPkgData']
8
9from storm.locals import (
10 Pickle,
11 Storm,
12 Unicode,
13 )
14from zope.interface import (
15 classProvides,
16 implements,
17 )
18
19from canonical.database.datetimecol import UtcDateTimeCol
20from lp.services.session.interfaces import IUseSessionStore
21
22
23class SessionData(Storm):
24 """A user's Session."""
25
26 classProvides(IUseSessionStore)
27 implements(IUseSessionStore)
28
29 __storm_table__ = 'SessionData'
30 client_id = Unicode(primary=True)
31 created = UtcDateTimeCol()
32 last_accessed = UtcDateTimeCol()
33
34
35class SessionPkgData(Storm):
36 """Data storage for a Session."""
37
38 classProvides(IUseSessionStore)
39 implements(IUseSessionStore)
40
41 __storm_table__ = 'SessionPkgData'
42 __storm_primary__ = 'client_id', 'product_id', 'key'
43
44 client_id = Unicode()
45 product_id = Unicode()
46 key = Unicode()
47 pickle = Pickle()
048
=== added directory 'lib/lp/services/session/tests'
=== added file 'lib/lp/services/session/tests/__init__.py'
=== added file 'lib/lp/services/session/tests/test_session.py'
--- lib/lp/services/session/tests/test_session.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/session/tests/test_session.py 2011-03-07 15:37:44 +0000
@@ -0,0 +1,32 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Session tests."""
5
6__metaclass__ = type
7
8from canonical.launchpad.interfaces.lpstorm import (
9 IMasterStore,
10 ISlaveStore,
11 IStore,
12 )
13from canonical.testing.layers import DatabaseFunctionalLayer
14from lp.services.session.model import (
15 SessionData,
16 SessionPkgData,
17 )
18from lp.testing import TestCase
19
20
21class TestSessionModelAdapters(TestCase):
22 layer = DatabaseFunctionalLayer
23
24 def test_adapters(self):
25 for adapter in [IMasterStore, ISlaveStore, IStore]:
26 for cls in [SessionData, SessionPkgData]:
27 for obj in [cls, cls()]:
28 store = adapter(obj)
29 self.assert_(
30 'session' in store.get_database()._dsn,
31 'Unknown store returned adapting %r to %r'
32 % (obj, adapter))
033
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2011-03-06 23:15:05 +0000
+++ lib/lp/testing/factory.py 2011-03-07 15:37:44 +0000
@@ -101,6 +101,7 @@
101 )101 )
102from canonical.launchpad.webapp.sorting import sorted_version_numbers102from canonical.launchpad.webapp.sorting import sorted_version_numbers
103from lp.app.enums import ServiceUsage103from lp.app.enums import ServiceUsage
104from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
104from lp.archiveuploader.dscfile import DSCFile105from lp.archiveuploader.dscfile import DSCFile
105from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy106from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy
106from lp.blueprints.enums import (107from lp.blueprints.enums import (
@@ -3850,6 +3851,20 @@
3850 description = self.getUniqueString()3851 description = self.getUniqueString()
3851 return getUtility(ICveSet).new(sequence, description, cvestate)3852 return getUtility(ICveSet).new(sequence, description, cvestate)
38523853
3854 def makePublisherConfig(self, distribution=None, root_dir=None,
3855 base_url=None, copy_base_url=None):
3856 """Create a new `PublisherConfig` record."""
3857 if distribution is None:
3858 distribution = self.makeDistribution()
3859 if root_dir is None:
3860 root_dir = self.getUniqueString()
3861 if base_url is None:
3862 base_url = self.getUniqueString()
3863 if copy_base_url is None:
3864 copy_base_url = self.getUniqueString()
3865 return getUtility(IPublisherConfigSet).new(
3866 distribution, root_dir, base_url, copy_base_url)
3867
38533868
3854# Some factory methods return simple Python types. We don't add3869# Some factory methods return simple Python types. We don't add
3855# security wrappers for them, as well as for objects created by3870# security wrappers for them, as well as for objects created by
38563871
=== modified file 'lib/lp/testing/tests/test_standard_test_template.py'
--- lib/lp/testing/tests/test_standard_test_template.py 2011-02-02 13:19:02 +0000
+++ lib/lp/testing/tests/test_standard_test_template.py 2011-03-07 15:37:44 +0000
@@ -6,15 +6,16 @@
6__metaclass__ = type6__metaclass__ = type
77
8from canonical.testing.layers import DatabaseFunctionalLayer8from canonical.testing.layers import DatabaseFunctionalLayer
9from lp.testing import TestCase # or TestCaseWithFactory9# or TestCaseWithFactory
10from lp.testing import TestCase
1011
1112
12class TestSomething(TestCase):13class TestSomething(TestCase):
13 # XXX: Sample test class. Replace with your own test class(es).14 # XXX: Sample test class. Replace with your own test class(es).
1415
15 # XXX: Optional layer--see lib/canonical/testing/layers.py16 # XXX: layer--see lib/canonical/testing/layers.py
16 # Get the simplest layer that your test will work on, or if you17 # Get the simplest layer that your test will work on. For unit tests
17 # don't even use the database, don't set it at all.18 # requiring no resources, this is BaseLayer.
18 layer = DatabaseFunctionalLayer19 layer = DatabaseFunctionalLayer
1920
20 # XXX: Sample test. Replace with your own test methods.21 # XXX: Sample test. Replace with your own test methods.