Merge lp:~sinzui/launchpad/listify-cached-licences into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 14797
Proposed branch: lp:~sinzui/launchpad/listify-cached-licences
Merge into: lp:launchpad
Diff against target: 286 lines (+164/-24)
5 files modified
lib/lp/registry/browser/person.py (+21/-6)
lib/lp/registry/browser/tests/test_commercialsubscription.py (+96/-3)
lib/lp/registry/tests/test_commercialprojects_vocabularies.py (+21/-6)
lib/lp/registry/vocabularies.py (+10/-9)
lib/lp/testing/__init__.py (+16/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/listify-cached-licences
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+92893@code.launchpad.net

Description of the change

Listify the precached commercial licenses.

    Launchpad bug: https://bugs.launchpad.net/bugs/931479
    Pre-implementation: jcsackett

+voucher has always had a performance problem because the saleforce
voucher data is slow to collect, the data is cached. When a voucher is
applied to add the commercial subscription to the projects, the view
issues a next_url signal to itself to reload the data cached by
setUpFields and SetUpWidgets. This breaks because I switched the
Commercial Admin query (not the query used by most users) to use
productset.search which is fast and has cached goodness. next_url is in
conflict with the model cache which does not want to reload the object.

Module zope.app.form.browser.itemswidgets, line 122, in convertTokensToValues
term = self.vocabulary.getTermByToken(token)
Module lp.registry.vocabularies, line 1552, in getTermByToken
for search_result in search_results:
Module lp.services.database.decoratedresultset, line 112, in __iter__
self.pre_iter_hook(results)
Module lp.registry.model.product, line 1602, in eager_load
cache._cached_licenses.append(license.license)
AttributeError: 'tuple' object has no attribute 'append'

--------------------------------------------------------------------

RULES

    * The cache issue was caused more by the vocabulary than by the
      view. The view calls the vocabulary multiple times during field and
      widget setup. getTermByToken() performs a search of search
      was previously called during a iter operation. Search is not needed
      in the case of for commercial admins...the token must be an active
      project name and that is faster to lookup that doing another
      search.
      * Do not search for tokens when the user is a commercial_admin. We
        only care if the token is for an active project.
      * Avoid setting next_url. Setup the fields and widgets again
        without the redeemed voucher.

QA

    * As a commercial admin, apply a voucher on qastaging for any project

LINT

    lib/lp/registry/vocabularies.py
    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/tests/test_commercialsubscription.py
    lib/lp/registry/tests/test_commercialprojects_vocabularies.py
    lib/lp/testing/__init__.py

TEST

    ./bin/test -vvc -t test_commercial -t voucher lp.registry
    ^ This also runs xx-voucher-redemption.txt which was crucial in
    identifing identify the need to regregister the current utility.

IMPLEMENTATION

Update getTermByToken() to just get the product for the token. Removed
a function that obsucured the number of calls to _do_search(). This subtle
change allowed me to redeem vouchers.
    lib/lp/registry/vocabularies.py
    lib/lp/registry/tests/test_commercialprojects_vocabularies.py

I did not need to do this set of changes to unblock my work as a
commercial admin. I see timeouts that I was certain I could fix by
removing the redeemed voucher from the view's cache instead of reloading
the whole view. This was not as simple as I thought it would be because
The fields and wdigets needed to be setup again *without* the submitted
form. I also changed redeemable_vouchers to guarantee a list since the
value could be None or just a voucher. I updated the FakeAdapterMixin
with a help that lets me register a test utility.
    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/tests/test_commercialsubscription.py
    lib/lp/testing/__init__.py

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :
Download full text (10.3 KiB)

Curtis--

This looks good. I have a few little concerns in the diff below, but they're not blockers to approval. I would like to see them address before landing, though.

> === modified file 'lib/lp/registry/browser/person.py'
> --- lib/lp/registry/browser/person.py 2012-02-11 04:45:42 +0000
> +++ lib/lp/registry/browser/person.py 2012-02-14 00:55:20 +0000
> @@ -2139,10 +2141,27 @@
>
> @cachedproperty
> def redeemable_vouchers(self):
> - """Get the redeemable vouchers owned by the user."""
> - vouchers = self.context.getRedeemableCommercialSubscriptionVouchers()
> + """Get the list redeemable vouchers owned by the user."""
> + vouchers = [
> + voucher for voucher in
> + self.context.getRedeemableCommercialSubscriptionVouchers()]
> return vouchers
>
> + def updateRedeemableVouchers(self, voucher):
> + """Remove the voucher from the cached list of redeemable vouchers.
> +
> + Updated the voucher field and widget so that the form can be reused.
> + """
> + vouchers = get_property_cache(self).redeemable_vouchers
> + vouchers.remove(voucher)
> + # Setup the fields and widgets again, but withut the submitted data.
> + self.form_fields = (
> + self.createProjectField() + self.createVoucherField())
> + self.widgets = form.setUpWidgets(
> + self.form_fields.select('project', 'voucher'),
> + self.prefix, self.context, self.request,
> + data=self.initial_values, ignore_request=True)

This is, at heart, a nitpick. Feel free to ignore it if you disagree. It
seems a little weird to name it "updateRedeemableVouchers" when the update is a secondary
effect of removing the voucher to keep the form working. Could we just
call this removeRedeemableVoucher, and note that the update occurs to keep
things in sync?

> @cachedproperty
> def has_commercial_projects(self):
> """Does the user manage one or more commercial project?
> @@ -2181,7 +2200,8 @@
> # Force the page to reload so the just consumed voucher is
> # not displayed again (since the field has already been
> # created).
> - self.next_url = self.request.URL
> + self.updateRedeemableVouchers(voucher)
> + #self.next_url = self.request.URL
> except SalesforceVoucherProxyException, error:
> self.addError(
> _("The voucher could not be redeemed at this time."))

If we're commenting this line out, shouldn't we just delete it?

> === modified file 'lib/lp/registry/browser/tests/test_commercialsubscription.py'
> --- lib/lp/registry/browser/tests/test_commercialsubscription.py 2012-02-09 18:49:14 +0000
> +++ lib/lp/registry/browser/tests/test_commercialsubscription.py 2012-02-14 00:55:20 +0000
> @@ -5,20 +5,113 @@
>
> __metaclass__ = type
>
> +from lp.services.salesforce.interfaces import ISalesforceVoucherProxy
> +from lp.services.salesforce.tests.proxy import TestSalesforceVoucherProxy
> from lp.services.webapp.publisher import canonical_url
> -from lp.testing import TestCaseWithFactory
> +from lp.te...

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/person.py'
2--- lib/lp/registry/browser/person.py 2012-02-11 04:45:42 +0000
3+++ lib/lp/registry/browser/person.py 2012-02-14 15:36:20 +0000
4@@ -103,6 +103,7 @@
5 queryMultiAdapter,
6 )
7 from zope.error.interfaces import IErrorReportingUtility
8+from zope.formlib import form
9 from zope.formlib.form import FormFields
10 from zope.interface import (
11 classImplements,
12@@ -2139,10 +2140,27 @@
13
14 @cachedproperty
15 def redeemable_vouchers(self):
16- """Get the redeemable vouchers owned by the user."""
17- vouchers = self.context.getRedeemableCommercialSubscriptionVouchers()
18+ """Get the list redeemable vouchers owned by the user."""
19+ vouchers = [
20+ voucher for voucher in
21+ self.context.getRedeemableCommercialSubscriptionVouchers()]
22 return vouchers
23
24+ def removeRedeemableVoucher(self, voucher):
25+ """Remove the voucher from the cached list of redeemable vouchers.
26+
27+ Updated the voucher field and widget so that the form can be reused.
28+ """
29+ vouchers = get_property_cache(self).redeemable_vouchers
30+ vouchers.remove(voucher)
31+ # Setup the fields and widgets again, but withut the submitted data.
32+ self.form_fields = (
33+ self.createProjectField() + self.createVoucherField())
34+ self.widgets = form.setUpWidgets(
35+ self.form_fields.select('project', 'voucher'),
36+ self.prefix, self.context, self.request,
37+ data=self.initial_values, ignore_request=True)
38+
39 @cachedproperty
40 def has_commercial_projects(self):
41 """Does the user manage one or more commercial project?
42@@ -2178,10 +2196,7 @@
43 subscription_months=voucher.term_months)
44 self.request.response.addInfoNotification(
45 _("Voucher redeemed successfully"))
46- # Force the page to reload so the just consumed voucher is
47- # not displayed again (since the field has already been
48- # created).
49- self.next_url = self.request.URL
50+ self.removeRedeemableVoucher(voucher)
51 except SalesforceVoucherProxyException, error:
52 self.addError(
53 _("The voucher could not be redeemed at this time."))
54
55=== modified file 'lib/lp/registry/browser/tests/test_commercialsubscription.py'
56--- lib/lp/registry/browser/tests/test_commercialsubscription.py 2012-02-09 18:49:14 +0000
57+++ lib/lp/registry/browser/tests/test_commercialsubscription.py 2012-02-14 15:36:20 +0000
58@@ -5,20 +5,113 @@
59
60 __metaclass__ = type
61
62+from lp.services.salesforce.interfaces import ISalesforceVoucherProxy
63+from lp.services.salesforce.tests.proxy import TestSalesforceVoucherProxy
64 from lp.services.webapp.publisher import canonical_url
65-from lp.testing import TestCaseWithFactory
66+from lp.testing import (
67+ FakeAdapterMixin,
68+ login_celebrity,
69+ login_person,
70+ TestCaseWithFactory,
71+ )
72 from lp.testing.layers import DatabaseFunctionalLayer
73 from lp.testing.views import create_initialized_view
74
75
76-class PersonVouchersViewTestCase(TestCaseWithFactory):
77+class PersonVouchersViewTestCase(FakeAdapterMixin, TestCaseWithFactory):
78 layer = DatabaseFunctionalLayer
79
80- def test_init(self):
81+ def test_init_without_vouchers_or_projects(self):
82+ # Thhe view provides common view properties, but the form is disabled.
83 user = self.factory.makePerson()
84 self.factory.makeProduct(owner=user)
85+ voucher_proxy = TestSalesforceVoucherProxy()
86+ self.registerUtility(voucher_proxy, ISalesforceVoucherProxy)
87 user_url = canonical_url(user)
88 view = create_initialized_view(user, '+vouchers')
89 self.assertEqual('Commercial subscription vouchers', view.page_title)
90 self.assertEqual(user_url, view.cancel_url)
91 self.assertIs(None, view.next_url)
92+ self.assertEqual(0, len(view.redeemable_vouchers))
93+ self.assertEqual([], view.form_fields)
94+
95+ def test_init_with_vouchers_and_projects(self):
96+ # The fields are setup when the user hase both vouchers and projects.
97+ user = self.factory.makePerson()
98+ login_person(user)
99+ voucher_proxy = TestSalesforceVoucherProxy()
100+ voucher_proxy.grantVoucher(
101+ user, user, user, 12)
102+ self.registerUtility(voucher_proxy, ISalesforceVoucherProxy)
103+ self.factory.makeProduct(owner=user)
104+ view = create_initialized_view(user, '+vouchers')
105+ self.assertEqual(1, len(view.redeemable_vouchers))
106+ self.assertEqual(
107+ ['project', 'voucher'], [f.__name__ for f in view.form_fields])
108+
109+ def test_init_with_commercial_admin_with_vouchers(self):
110+ # The fields are setup if the commercial admin has vouchers.
111+ commercial_admin = login_celebrity('commercial_admin')
112+ voucher_proxy = TestSalesforceVoucherProxy()
113+ voucher_proxy.grantVoucher(
114+ commercial_admin, commercial_admin, commercial_admin, 12)
115+ self.registerUtility(voucher_proxy, ISalesforceVoucherProxy)
116+ view = create_initialized_view(commercial_admin, '+vouchers')
117+ self.assertEqual(1, len(view.redeemable_vouchers))
118+ self.assertEqual(
119+ ['project', 'voucher'], [f.__name__ for f in view.form_fields])
120+
121+ def test_redeem_with_commercial_admin(self):
122+ # The fields are setup if the commercial admin has vouchers.
123+ commercial_admin = login_celebrity('commercial_admin')
124+ voucher_proxy = TestSalesforceVoucherProxy()
125+ voucher_id = voucher_proxy.grantVoucher(
126+ commercial_admin, commercial_admin, commercial_admin, 12)
127+ self.registerUtility(voucher_proxy, ISalesforceVoucherProxy)
128+ project = self.factory.makeProduct()
129+ form = {
130+ 'field.project': project.name,
131+ 'field.voucher': voucher_id,
132+ 'field.actions.redeem': 'Redeem',
133+ }
134+ view = create_initialized_view(
135+ commercial_admin, '+vouchers', form=form)
136+ self.assertEqual([], view.errors)
137+ self.assertIsNot(None, project.commercial_subscription)
138+ self.assertEqual(0, len(view.redeemable_vouchers))
139+ self.assertEqual(
140+ 0, len(view.form_fields['voucher'].field.vocabulary))
141+ self.assertEqual(
142+ 0, len(view.widgets['voucher'].vocabulary))
143+
144+ def test_redeem_twice_with_commercial_admin(self):
145+ # The fields are setup if the commercial admin has vouchers.
146+ commercial_admin = login_celebrity('commercial_admin')
147+ voucher_proxy = TestSalesforceVoucherProxy()
148+ voucher_id_1 = voucher_proxy.grantVoucher(
149+ commercial_admin, commercial_admin, commercial_admin, 12)
150+ voucher_id_2 = voucher_proxy.grantVoucher(
151+ commercial_admin, commercial_admin, commercial_admin, 12)
152+ self.registerUtility(voucher_proxy, ISalesforceVoucherProxy)
153+ project_1 = self.factory.makeProduct()
154+ project_2 = self.factory.makeProduct()
155+ form = {
156+ 'field.project': project_1.name,
157+ 'field.voucher': voucher_id_1,
158+ 'field.actions.redeem': 'Redeem',
159+ }
160+ view = create_initialized_view(
161+ commercial_admin, '+vouchers', form=form)
162+ self.assertEqual([], view.errors)
163+ self.assertIsNot(None, project_1.commercial_subscription)
164+ self.assertEqual(1, len(view.redeemable_vouchers))
165+ form = {
166+ 'field.project': project_2.name,
167+ 'field.voucher': voucher_id_2,
168+ 'field.actions.redeem': 'Redeem',
169+ }
170+ view = create_initialized_view(
171+ commercial_admin, '+vouchers', form=form)
172+ self.assertEqual([], view.errors)
173+ self.assertIsNot(None, project_2.commercial_subscription)
174+ self.assertEqual(0, len(view.redeemable_vouchers))
175
176=== modified file 'lib/lp/registry/tests/test_commercialprojects_vocabularies.py'
177--- lib/lp/registry/tests/test_commercialprojects_vocabularies.py 2012-02-10 21:54:12 +0000
178+++ lib/lp/registry/tests/test_commercialprojects_vocabularies.py 2012-02-14 15:36:20 +0000
179@@ -116,16 +116,31 @@
180 self.assertEqual(
181 'Open-widget (expires %s)' % expiration_date, term.title)
182
183- def test_getTermByToken(self):
184- # The term for a token in the vocabulary is returned.
185- token = self.vocab.getTermByToken('open-widget')
186- self.assertEqual(self.maintained_project, token.value)
187-
188- def test_getTermByToken_error(self):
189+ def test_getTermByToken_user(self):
190+ # The term for a token in the vocabulary is returned for maintained
191+ # projects.
192+ token = self.vocab.getTermByToken('open-widget')
193+ self.assertEqual(self.maintained_project, token.value)
194+
195+ def test_getTermByToken_commercial_admin(self):
196+ # The term for a token in the vocabulary is returned for any
197+ # active project.
198+ login_celebrity('commercial_admin')
199+ token = self.vocab.getTermByToken('open-widget')
200+ self.assertEqual(self.maintained_project, token.value)
201+
202+ def test_getTermByToken_error_user(self):
203 # A LookupError is raised if the token is not in the vocabulary.
204 self.assertRaises(
205 LookupError, self.vocab.getTermByToken, 'norwegian-blue-widget')
206
207+ def test_getTermByToken_error_commercial_admin(self):
208+ # The term for a token in the vocabulary is returned for any
209+ # active project.
210+ login_celebrity('commercial_admin')
211+ self.assertRaises(
212+ LookupError, self.vocab.getTermByToken, 'norwegian-blue-widget')
213+
214 def test_iter(self):
215 # The vocabulary can be iterated and the order is by displayname.
216 displaynames = [p.value.displayname for p in self.vocab]
217
218=== modified file 'lib/lp/registry/vocabularies.py'
219--- lib/lp/registry/vocabularies.py 2012-02-10 21:54:12 +0000
220+++ lib/lp/registry/vocabularies.py 2012-02-14 15:36:20 +0000
221@@ -1548,10 +1548,15 @@
222
223 def getTermByToken(self, token):
224 """Return the term for the given token."""
225- search_results = self._doSearch(token)
226- for search_result in search_results:
227- if search_result.name == token:
228- return self.toTerm(search_result)
229+ if self.is_commercial_admin:
230+ project = self.product_set.getByName(token)
231+ if project is not None and project.active:
232+ return self.toTerm(project)
233+ else:
234+ search_results = self._doSearch(token)
235+ for search_result in search_results:
236+ if search_result.name == token:
237+ return self.toTerm(search_result)
238 raise LookupError(token)
239
240 def searchForTerms(self, query=None, vocab_filter=None):
241@@ -1560,13 +1565,9 @@
242 num = results.count()
243 return CountableIterator(num, results, self.toTerm)
244
245- def _commercial_projects(self):
246- """Return the list of commercial projects owned by this user."""
247- return self._doSearch()
248-
249 def __iter__(self):
250 """See `IVocabulary`."""
251- for proj in self._commercial_projects():
252+ for proj in self._doSearch():
253 yield self.toTerm(proj)
254
255 def __contains__(self, project):
256
257=== modified file 'lib/lp/testing/__init__.py'
258--- lib/lp/testing/__init__.py 2012-02-10 03:56:09 +0000
259+++ lib/lp/testing/__init__.py 2012-02-14 15:36:20 +0000
260@@ -107,6 +107,7 @@
261 from testtools.testcase import ExpectedException as TTExpectedException
262 import transaction
263 from zope.component import (
264+ ComponentLookupError,
265 getMultiAdapter,
266 getSiteManager,
267 getUtility,
268@@ -1519,3 +1520,18 @@
269
270 def getAdapter(self, for_interfaces, provided_interface, name=None):
271 return getMultiAdapter(for_interfaces, provided_interface, name=name)
272+
273+ def registerUtility(self, component, for_interface, name=''):
274+ try:
275+ current_commponent = getUtility(for_interface, name=name)
276+ except ComponentLookupError:
277+ current_commponent = None
278+ site_manager = getSiteManager()
279+ site_manager.registerUtility(component, for_interface, name)
280+ self.addCleanup(
281+ site_manager.unregisterUtility, component, for_interface, name)
282+ if current_commponent is not None:
283+ # Restore the default utility.
284+ self.addCleanup(
285+ site_manager.registerUtility, current_commponent,
286+ for_interface, name)