launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #13505
[Merge] lp:~wallyworld/launchpad/vouchers-timeout-1014641 into lp:launchpad
Ian Booth has proposed merging lp:~wallyworld/launchpad/vouchers-timeout-1014641 into lp:launchpad.
Commit message:
Allow request start time to be reset and add a decorator to eliminate external calls to salesforce from request timeout calculations.
Requested reviews:
Curtis Hovey (sinzui)
Related bugs:
Bug #1014641 in Launchpad itself: "+vouchers times out"
https://bugs.launchpad.net/launchpad/+bug/1014641
For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/vouchers-timeout-1014641/+merge/129862
== Implementation ==
This branch changes the workflow of how vouchers are redeemed. The redemption via Salesforce has been moved from in-line to a garbo job. Viewing highlights:
- redeeming a voucher updates/creates the commercial subscription record and sets the sales_force_id to 'pending-xxxxx' where xxxx is the voucher id.
- the voucher is not redeemed immediately in salesforce
- the +vouchers view has been changed to remove from the salesforce unredeemed vouchers list any pending vouchers so that someone can not accidentally try and re-redeem a pending voucher
- any attempt to redeem a voucher which has already been submitted (because someone else got in first) results in an field error being displayed on the form
- a garbo job runs frequently to redeem pending vouchers in salesforce and update the commercial subscription record in Launchpad
== Tests ==
Add test for new garbo job.
Add tests for new view behaviour regarding not showing pending vouchers and handling submission of vouchers which have been already processed
Update some doctests which repeatedly submit the same voucher id for different projects because this is no longer allowed.
== Lint ==
Checking for conflicts and issues in changed files.
Linting changed files:
database/schema/security.cfg
lib/lp/registry/errors.py
lib/lp/registry/browser/person.py
lib/lp/registry/browser/tests/test_commercialsubscription.py
lib/lp/registry/doc/commercialsubscription.txt
lib/lp/registry/model/person.py
lib/lp/registry/model/product.py
lib/lp/registry/templates/person-vouchers.pt
lib/lp/scripts/garbo.py
lib/lp/scripts/tests/test_garbo.py
lib/lp/testing/factory.py
--
https://code.launchpad.net/~wallyworld/launchpad/vouchers-timeout-1014641/+merge/129862
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2012-10-11 06:53:58 +0000
+++ database/schema/security.cfg 2012-10-18 01:47:26 +0000
@@ -2251,7 +2251,7 @@
public.codeimportevent = SELECT, DELETE
public.codeimporteventdata = SELECT, DELETE
public.codeimportresult = SELECT, DELETE
-public.commercialsubscription = SELECT
+public.commercialsubscription = SELECT, UPDATE
public.emailaddress = SELECT, UPDATE, DELETE
public.hwsubmission = SELECT, UPDATE
public.job = SELECT, INSERT, DELETE
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2012-10-15 04:51:12 +0000
+++ lib/lp/registry/browser/person.py 2012-10-18 01:47:26 +0000
@@ -157,6 +157,7 @@
)
from lp.registry.browser.teamjoin import TeamJoinMixin
from lp.registry.enums import PersonVisibility
+from lp.registry.errors import VoucherAlreadyRedeemed
from lp.registry.interfaces.codeofconduct import ISignedCodeOfConductSet
from lp.registry.interfaces.gpg import IGPGKeySet
from lp.registry.interfaces.irc import IIrcIDSet
@@ -230,6 +231,7 @@
)
from lp.services.salesforce.interfaces import (
ISalesforceVoucherProxy,
+ REDEEMABLE_VOUCHER_STATUSES,
SalesforceVoucherProxyException,
)
from lp.services.verification.interfaces.authtoken import LoginTokenType
@@ -1257,10 +1259,7 @@
"""Set up the fields for this view."""
self.form_fields = []
- # Make the less expensive test for commercial projects first
- # to avoid the more costly fetching of redeemable vouchers.
- if (self.has_commercial_projects and
- len(self.redeemable_vouchers) > 0):
+ if self.has_commercial_projects:
self.form_fields = (self.createProjectField() +
self.createVoucherField())
@@ -1343,17 +1342,25 @@
voucher = data['voucher']
try:
- # The call to redeemVoucher returns True if it succeeds or it
- # raises an exception. Therefore the return value does not need
- # to be checked.
- salesforce_proxy.redeemVoucher(voucher.voucher_id,
- self.context,
- project)
- project.redeemSubscriptionVoucher(
- voucher=voucher.voucher_id,
- registrant=self.context,
- purchaser=self.context,
- subscription_months=voucher.term_months)
+ # Perform a check that the submitted voucher id is valid.
+ check_voucher = salesforce_proxy.getVoucher(voucher.voucher_id)
+ if not check_voucher.status in REDEEMABLE_VOUCHER_STATUSES:
+ self.addError(
+ _("Voucher %s has invalid status %s"
+ % (check_voucher.voucher_id, check_voucher.status)))
+ return
+ # Redeem the voucher in Launchpad, marking the subscription as
+ # pending. Launchpad will honour the subscription but a job will
+ # still need to be run to notify Salesforce.
+ try:
+ project.redeemSubscriptionVoucher(
+ voucher='pending-' + voucher.voucher_id,
+ registrant=self.context,
+ purchaser=self.context,
+ subscription_months=voucher.term_months)
+ except VoucherAlreadyRedeemed as error:
+ self.setFieldError('voucher', _(error.message))
+ return
self.request.response.addInfoNotification(
_("Voucher redeemed successfully"))
self.removeRedeemableVoucher(voucher)
=== modified file 'lib/lp/registry/browser/tests/test_commercialsubscription.py'
--- lib/lp/registry/browser/tests/test_commercialsubscription.py 2012-02-13 23:54:02 +0000
+++ lib/lp/registry/browser/tests/test_commercialsubscription.py 2012-10-18 01:47:26 +0000
@@ -12,9 +12,14 @@
FakeAdapterMixin,
login_celebrity,
login_person,
+ person_logged_in,
TestCaseWithFactory,
)
from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.pages import (
+ extract_text,
+ find_tags_by_class,
+ )
from lp.testing.views import create_initialized_view
@@ -105,6 +110,11 @@
self.assertEqual([], view.errors)
self.assertIsNot(None, project_1.commercial_subscription)
self.assertEqual(1, len(view.redeemable_vouchers))
+ # A job will notify Salesforce of the voucher redemption but here we
+ # will do it manually.
+ voucher_proxy.redeemVoucher(
+ voucher_id_1, commercial_admin, project_1)
+
form = {
'field.project': project_2.name,
'field.voucher': voucher_id_2,
@@ -115,3 +125,53 @@
self.assertEqual([], view.errors)
self.assertIsNot(None, project_2.commercial_subscription)
self.assertEqual(0, len(view.redeemable_vouchers))
+
+ def test_pending_vouchers_excluded(self):
+ # Vouchers pending redemption in Salesforce are not included in choice.
+ commercial_admin = login_celebrity('commercial_admin')
+ voucher_proxy = TestSalesforceVoucherProxy()
+ voucher_id_1 = voucher_proxy.grantVoucher(
+ commercial_admin, commercial_admin, commercial_admin, 12)
+ voucher_id_2 = voucher_proxy.grantVoucher(
+ commercial_admin, commercial_admin, commercial_admin, 12)
+ self.registerUtility(voucher_proxy, ISalesforceVoucherProxy)
+ project_1 = self.factory.makeProduct()
+ self.factory.makeCommercialSubscription(
+ project_1, False, 'pending-' + voucher_id_1)
+ view = create_initialized_view(commercial_admin, '+vouchers')
+ vouchers = list(view.widgets['voucher'].vocabulary)
+ # Only voucher2 in vocab since voucher1 is pending redemption.
+ self.assertEqual(1, len(vouchers))
+ self.assertEqual(voucher_id_2, vouchers[0].token)
+
+ def test_redeem_twice_causes_error(self):
+ # If a voucher is redeemed twice, the second attempt is rejected.
+ commercial_admin = login_celebrity('commercial_admin')
+ voucher_proxy = TestSalesforceVoucherProxy()
+ voucher_id_1 = voucher_proxy.grantVoucher(
+ commercial_admin, commercial_admin, commercial_admin, 12)
+ self.registerUtility(voucher_proxy, ISalesforceVoucherProxy)
+ project_1 = self.factory.makeProduct(name='p1')
+ project_2 = self.factory.makeProduct(name='p2')
+ url = canonical_url(commercial_admin, view_name='+vouchers')
+ browser = self.getUserBrowser(url, commercial_admin)
+ # A second browser opens the +vouchers page before the first browser
+ # attempts to redeem the voucher.
+ browser2 = self.getUserBrowser(url, commercial_admin)
+ browser.getControl(
+ 'Select the project you wish to subscribe').value = 'p1'
+ browser.getControl(
+ 'Select a voucher').getControl(voucher_id_1).selected = True
+ browser.getControl('Redeem').click()
+ with person_logged_in(commercial_admin):
+ self.assertIsNotNone(project_1.commercial_subscription)
+
+ browser2.getControl(
+ 'Select the project you wish to subscribe').value = 'p2'
+ browser2.getControl(
+ 'Select a voucher').getControl(voucher_id_1).selected = True
+ browser2.getControl('Redeem').click()
+ with person_logged_in(commercial_admin):
+ self.assertIsNone(project_2.commercial_subscription)
+ error_messages = find_tags_by_class(browser2.contents, 'message')
+ self.assertEqual(extract_text(error_messages[1]), 'Invalid value')
=== modified file 'lib/lp/registry/doc/commercialsubscription.txt'
--- lib/lp/registry/doc/commercialsubscription.txt 2012-10-09 19:04:40 +0000
+++ lib/lp/registry/doc/commercialsubscription.txt 2012-10-18 01:47:26 +0000
@@ -35,6 +35,12 @@
>>> print bzr.commercial_subscription.sales_system_id
asdf123
+If a voucher is attempted to be redeemed twice, an error occurs.
+ >>> bzr.redeemSubscriptionVoucher('asdf123', owner, owner, 12)
+ Traceback (most recent call last):
+ ...
+ VoucherAlreadyRedeemed: Voucher asdf123 has already been redeemed for Bazaar
+
Commercial subscriptions have zope.Public permissions for reading.
>>> from lp.services.webapp.authorization import check_permission
@@ -143,7 +149,7 @@
>>> subscription.date_expires = datetime(2009,11,15,tzinfo=UTC)
>>> old_date_expires = bzr.commercial_subscription.date_expires
- >>> bzr.redeemSubscriptionVoucher('foo456', owner, owner, 2,
+ >>> bzr.redeemSubscriptionVoucher('foo1456', owner, owner, 2,
... 'notes', current_datetime=now)
>>> print subscription.date_expires
2010-01-15...
@@ -152,7 +158,7 @@
>>> subscription.date_expires = datetime(2009,11,15,tzinfo=UTC)
>>> old_date_expires = bzr.commercial_subscription.date_expires
- >>> bzr.redeemSubscriptionVoucher('foo456', owner, owner, 11,
+ >>> bzr.redeemSubscriptionVoucher('foo2456', owner, owner, 11,
... 'notes', current_datetime=now)
>>> print subscription.date_expires
2010-10-15...
@@ -165,7 +171,7 @@
>>> subscription.date_expires = datetime(2009,10,31,tzinfo=UTC)
>>> old_date_expires = bzr.commercial_subscription.date_expires
- >>> bzr.redeemSubscriptionVoucher('foo456', owner, owner, 4,
+ >>> bzr.redeemSubscriptionVoucher('foo3456', owner, owner, 4,
... 'notes', current_datetime=now)
>>> print subscription.date_expires
2010-02-28...
@@ -175,7 +181,7 @@
>>> subscription.date_expires = datetime(2011,10,31,tzinfo=UTC)
>>> old_date_expires = bzr.commercial_subscription.date_expires
- >>> bzr.redeemSubscriptionVoucher('foo456', owner, owner, 4,
+ >>> bzr.redeemSubscriptionVoucher('foo4456', owner, owner, 4,
... 'notes', current_datetime=now)
>>> print subscription.date_expires
2012-02-29...
@@ -186,7 +192,7 @@
>>> subscription.date_starts = datetime(2008,1,15,tzinfo=UTC)
>>> subscription.date_expires = datetime(2009,1,15,tzinfo=UTC)
>>> now = datetime(2010, 1, 1, tzinfo=UTC)
- >>> bzr.redeemSubscriptionVoucher('foo456', owner, owner, 12,
+ >>> bzr.redeemSubscriptionVoucher('foo5456', owner, owner, 12,
... 'notes', current_datetime=now)
>>> print subscription.date_expires
2011-01-01...
@@ -198,7 +204,7 @@
>>> subscription.date_starts = datetime(2008,1,15,tzinfo=UTC)
>>> subscription.date_expires = datetime(2009,1,15,tzinfo=UTC)
>>> now = datetime(2009, 1, 1, tzinfo=UTC)
- >>> bzr.redeemSubscriptionVoucher('foo456', owner, owner, 12,
+ >>> bzr.redeemSubscriptionVoucher('foo6456', owner, owner, 12,
... 'notes', current_datetime=now)
>>> print subscription.date_expires
2010-01-15...
=== modified file 'lib/lp/registry/errors.py'
--- lib/lp/registry/errors.py 2012-10-11 16:06:49 +0000
+++ lib/lp/registry/errors.py 2012-10-18 01:47:26 +0000
@@ -28,6 +28,7 @@
'TeamMembershipPolicyError',
'UserCannotChangeMembershipSilently',
'UserCannotSubscribePerson',
+ 'VoucherAlreadyRedeemed',
]
import httplib
@@ -207,3 +208,7 @@
class CannotPackageProprietaryProduct(Exception):
"""Raised when a non-PUBLIC product's series is linked to a package."""
+
+
+class VoucherAlreadyRedeemed(Exception):
+ """Raised when a voucher is redeemed more than once."""
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2012-10-16 23:42:55 +0000
+++ lib/lp/registry/model/person.py 2012-10-18 01:47:26 +0000
@@ -1137,14 +1137,28 @@
def getRedeemableCommercialSubscriptionVouchers(self, voucher_proxy=None):
"""See `IPerson`."""
+ # Circular imports.
+ from lp.registry.model.commercialsubscription import (
+ CommercialSubscription,
+ )
if voucher_proxy is None:
voucher_proxy = getUtility(ISalesforceVoucherProxy)
vouchers = voucher_proxy.getUnredeemedVouchers(self)
- for voucher in vouchers:
+ # Exclude pending vouchers being sent to Salesforce and vouchers which
+ # have already been redeemed.
+ voucher_ids = [unicode(voucher.voucher_id) for voucher in vouchers]
+ voucher_expr = (
+ "trim(leading 'pending-' "
+ "from CommercialSubscription.sales_system_id)")
+ already_redeemed = list(Store.of(self).using(CommercialSubscription)
+ .find(SQL(voucher_expr), SQL(voucher_expr).is_in(voucher_ids)))
+ redeemable_vouchers = [voucher for voucher in vouchers
+ if voucher.voucher_id not in already_redeemed]
+ for voucher in redeemable_vouchers:
assert voucher.status in REDEEMABLE_VOUCHER_STATUSES, (
"Voucher %s has invalid status %s" %
(voucher.voucher_id, voucher.status))
- return vouchers
+ return redeemable_vouchers
def hasCurrentCommercialSubscription(self):
"""See `IPerson`."""
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2012-10-16 14:28:22 +0000
+++ lib/lp/registry/model/product.py 2012-10-18 01:47:26 +0000
@@ -138,6 +138,7 @@
from lp.registry.errors import (
CannotChangeInformationType,
CommercialSubscribersOnly,
+ VoucherAlreadyRedeemed,
)
from lp.registry.interfaces.accesspolicy import (
IAccessPolicyArtifactSource,
@@ -753,6 +754,19 @@
day=new_day)
return new_date
+ # The voucher may already have been redeemed or marked as redeemed
+ # pending notification being sent to Salesforce.
+ voucher_expr = (
+ "trim(leading 'pending-' "
+ "from CommercialSubscription.sales_system_id)")
+ already_redeemed = Store.of(self).find(
+ CommercialSubscription,
+ SQL(voucher_expr) == unicode(voucher)).any()
+ if already_redeemed:
+ raise VoucherAlreadyRedeemed(
+ "Voucher %s has already been redeemed for %s"
+ % (voucher, already_redeemed.product.displayname))
+
if current_datetime is None:
current_datetime = datetime.datetime.now(pytz.timezone('UTC'))
=== modified file 'lib/lp/registry/templates/person-vouchers.pt'
--- lib/lp/registry/templates/person-vouchers.pt 2012-02-09 18:31:44 +0000
+++ lib/lp/registry/templates/person-vouchers.pt 2012-10-18 01:47:26 +0000
@@ -21,8 +21,10 @@
vouchers for a project.
</tal:not_commercial>
<tal:commercial condition="commercial_projects">
- <tal:define_vouchers define="vouchers view/redeemable_vouchers" >
- <tal:no_vouchers condition="not: vouchers">
+ <tal:define_vouchers
+ define="vouchers view/redeemable_vouchers;
+ view_errors view/errors" >
+ <tal:no_vouchers condition="python: not vouchers and not view_errors">
<p>
<tal:name replace="context/fmt:displayname" />
does not have any redeemable vouchers available.</p>
@@ -32,7 +34,7 @@
/>
</tal:no_vouchers>
- <tal:has_voucher condition="vouchers">
+ <tal:has_voucher condition="python: vouchers or view_errors">
<div metal:use-macro="context/@@launchpad_form/form">
<metal:widgets fill-slot="widgets">
<table class="form">
=== modified file 'lib/lp/scripts/garbo.py'
--- lib/lp/scripts/garbo.py 2012-10-12 14:53:10 +0000
+++ lib/lp/scripts/garbo.py 2012-10-18 01:47:26 +0000
@@ -29,6 +29,7 @@
import pytz
from storm.expr import (
In,
+ Like,
Select,
Update,
)
@@ -60,6 +61,7 @@
RevisionCache,
)
from lp.hardwaredb.model.hwdb import HWSubmission
+from lp.registry.model.commercialsubscription import CommercialSubscription
from lp.registry.model.person import Person
from lp.registry.model.product import Product
from lp.services.config import config
@@ -92,6 +94,10 @@
from lp.services.oauth.model import OAuthNonce
from lp.services.openid.model.openidconsumer import OpenIDConsumerNonce
from lp.services.propertycache import cachedproperty
+from lp.services.salesforce.interfaces import (
+ ISalesforceVoucherProxy,
+ SalesforceVoucherProxyException,
+ )
from lp.services.scripts.base import (
LaunchpadCronScript,
LOCK_PATH,
@@ -365,6 +371,58 @@
self.store.commit()
+class VoucherRedeemer(TunableLoop):
+ """Redeem pending sales vouchers with Salesforce."""
+ maximum_chunk_size = 5
+
+ voucher_expr = (
+ "trim(leading 'pending-' "
+ "from CommercialSubscription.sales_system_id)")
+
+ def __init__(self, log, abort_time=None):
+ super(VoucherRedeemer, self).__init__(log, abort_time)
+ self.store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
+
+ @cachedproperty
+ def _salesforce_proxy(self):
+ return getUtility(ISalesforceVoucherProxy)
+
+ @property
+ def _pending_subscriptions(self):
+ return self.store.find(
+ CommercialSubscription,
+ Like(CommercialSubscription.sales_system_id, u'pending-%')
+ )
+
+ def isDone(self):
+ return self._pending_subscriptions.count() == 0
+
+ def __call__(self, chunk_size):
+ successful_ids = []
+ for sub in self._pending_subscriptions[:chunk_size]:
+ sales_system_id = sub.sales_system_id[len('pending-'):]
+ try:
+ # The call to redeemVoucher returns True if it succeeds or it
+ # raises an exception. Therefore the return value does not
+ # need to be checked.
+ self._salesforce_proxy.redeemVoucher(
+ sales_system_id, sub.purchaser, sub.product)
+ successful_ids.append(unicode(sub.sales_system_id))
+ except SalesforceVoucherProxyException as error:
+ self.log.error(
+ "Failed to redeem voucher %s: %s"
+ % (sales_system_id, error.message))
+ # Update the successfully redeemed voucher ids to be not pending.
+ if successful_ids:
+ self.store.find(
+ CommercialSubscription,
+ CommercialSubscription.sales_system_id.is_in(successful_ids)
+ ).set(
+ CommercialSubscription.sales_system_id ==
+ SQL(self.voucher_expr))
+ transaction.commit()
+
+
class OpenIDConsumerNoncePruner(TunableLoop):
"""An ITunableLoop to prune old OpenIDConsumerNonce records.
@@ -1280,6 +1338,7 @@
OpenIDConsumerNoncePruner,
OpenIDConsumerAssociationPruner,
AntiqueSessionPruner,
+ VoucherRedeemer,
]
experimental_tunable_loops = []
=== modified file 'lib/lp/scripts/tests/test_garbo.py'
--- lib/lp/scripts/tests/test_garbo.py 2012-10-12 14:53:10 +0000
+++ lib/lp/scripts/tests/test_garbo.py 2012-10-18 01:47:26 +0000
@@ -17,6 +17,7 @@
from pytz import UTC
from storm.expr import (
In,
+ Like,
Min,
Not,
SQL,
@@ -59,6 +60,7 @@
)
from lp.registry.interfaces.accesspolicy import IAccessPolicySource
from lp.registry.interfaces.person import IPersonSet
+from lp.registry.model.commercialsubscription import CommercialSubscription
from lp.registry.model.product import Product
from lp.scripts.garbo import (
AntiqueSessionPruner,
@@ -97,6 +99,8 @@
OAuthNonce,
)
from lp.services.openid.model.openidconsumer import OpenIDConsumerNonce
+from lp.services.salesforce.interfaces import ISalesforceVoucherProxy
+from lp.services.salesforce.tests.proxy import TestSalesforceVoucherProxy
from lp.services.scripts.tests import run_script
from lp.services.session.model import (
SessionData,
@@ -106,6 +110,7 @@
from lp.services.verification.model.logintoken import LoginToken
from lp.services.worlddata.interfaces.language import ILanguageSet
from lp.testing import (
+ FakeAdapterMixin,
person_logged_in,
TestCase,
TestCaseWithFactory,
@@ -370,7 +375,7 @@
self.assertEqual(expected_sessions, found_sessions)
-class TestGarbo(TestCaseWithFactory):
+class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
layer = LaunchpadZopelessLayer
def setUp(self):
@@ -966,6 +971,36 @@
"SELECT COUNT(*) FROM BugSummaryJournal").get_one()[0]
self.assertThat(num_rows, Equals(0))
+ def test_VoucherRedeemer(self):
+ switch_dbuser('testadmin')
+ store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
+
+ voucher_proxy = TestSalesforceVoucherProxy()
+ self.registerUtility(voucher_proxy, ISalesforceVoucherProxy)
+
+ # Mark has some unredeemed vouchers so set one of them as pending.
+ mark = getUtility(IPersonSet).getByName('mark')
+ voucher = voucher_proxy.getUnredeemedVouchers(mark)[0]
+ product = self.factory.makeProduct(owner=mark)
+ redeemed_id = voucher.voucher_id
+ self.factory.makeCommercialSubscription(
+ product, False, 'pending-%s' % redeemed_id)
+ transaction.commit()
+
+ self.runFrequently()
+
+ # There should now be 0 pending vouchers in Launchpad.
+ num_rows = store.find(
+ CommercialSubscription,
+ Like(CommercialSubscription.sales_system_id, u'pending-%')
+ ).count()
+ self.assertThat(num_rows, Equals(0))
+ # Salesforce should also now have redeemed the voucher.
+ unredeemed_ids = [
+ voucher.voucher_id
+ for voucher in voucher_proxy.getUnredeemedVouchers(mark)]
+ self.assertNotIn(redeemed_id, unredeemed_ids)
+
def test_UnusedPOTMsgSetPruner_removes_obsolete_message_sets(self):
# UnusedPOTMsgSetPruner removes any POTMsgSet that are
# participating in a POTemplate only as obsolete messages.
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2012-10-16 23:41:23 +0000
+++ lib/lp/testing/factory.py 2012-10-18 01:47:26 +0000
@@ -4266,7 +4266,8 @@
}
return fileupload
- def makeCommercialSubscription(self, product, expired=False):
+ def makeCommercialSubscription(self, product, expired=False,
+ voucher_id='new'):
"""Create a commercial subscription for the given product."""
if CommercialSubscription.selectOneBy(product=product) is not None:
raise AssertionError(
@@ -4281,7 +4282,7 @@
date_expires=expiry,
registrant=product.owner,
purchaser=product.owner,
- sales_system_id='new',
+ sales_system_id=voucher_id,
whiteboard='')
def grantCommercialSubscription(self, person, months=12):
Follow ups