Merge lp:~edwin-grubbs/launchpad/bug-405300-remove-simplepopupwidget into lp:launchpad

Proposed by Edwin Grubbs
Status: Merged
Approved by: Paul Hummer
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~edwin-grubbs/launchpad/bug-405300-remove-simplepopupwidget
Merge into: lp:launchpad
Diff against target: None lines
To merge this branch: bzr merge lp:~edwin-grubbs/launchpad/bug-405300-remove-simplepopupwidget
Reviewer Review Type Date Requested Status
Paul Hummer (community) Approve
Martin Albisetti (community) ui Approve
Canonical Launchpad Engineering code Pending
Review via email: mp+9481@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (4.3 KiB)

Summary
-------

This branch removes the few remaining calls to SimplePopupWidget, and it
removes that widget's code, templates, tests, and the SimplePopupView that it
loads into an iframe.

Implementation details
----------------------

There are three changes in the this file:
    * Use the bullet icon for vocabulary items that don't have their own icon
    like bugs and people.
    * Get a description for the SourcePackageName
    items that keeps it identical to the SourcePackageNameVocabulary. The
    reason for the change is that the old popup widget would display the
    vocab item token on the first line and the title on the second line.
    The picker widget shows the title on the first line, and the second
    line is the description set by an adapter, or it's the obj.summary.
    * Catch the NoCanonicalUrl, since the SourcePackageName does not have
    a canonical url. The api_uri is only necessary for inline editing
    using the REST API. The form picker works fine without it.

    lib/canonical/launchpad/browser/vocabulary.py

Register adapter defined in the above file.
    lib/canonical/launchpad/zcml/launchpad.zcml

Deleted:
    lib/canonical/launchpad/doc/popup-view.txt
    lib/canonical/launchpad/doc/popup-widget.txt
    lib/canonical/launchpad/pagetests/standalone/xx-show-people-with-password-only.txt
    lib/canonical/widgets/templates/popup-window.pt
    lib/lp/soyuz/stories/soyuz/xx-search-for-binary-and-source-packages.txt

Changes to the SourcePackageNameVocabulary as described above.
    lib/canonical/launchpad/doc/vocabularies.txt

Add adapter so that the PersonPickerWidget is automatically used for
person form attributes. This is just necessary to add the
"Create Team" link after the "Choose" link.
    lib/canonical/launchpad/webapp/configure.zcml

Windmill tests:
    lib/canonical/launchpad/windmill/testing/widgets.py
    lib/lp/bugs/windmill/tests/test_bugs/test_bug_also_affects_new_upstream.py

Remove references to the old popup widget. The windmill test should
cover the most important aspect, but not test every form that had a
weak test for the old popup widget.
    lib/canonical/widgets/__init__.py
    lib/canonical/widgets/bugtask.py
    lib/canonical/widgets/project.py
    lib/canonical/launchpad/doc/project-scope-widget.txt
    lib/lp/bugs/stories/bug-also-affects/20-bug-requestupstreamfix.txt
    lib/lp/bugs/stories/bug-also-affects/xx-also-affects-new-upstream.txt

This was always unnecessary.
    lib/canonical/widgets/configure.zcml

Remove old widget and migrate necessary methods that are used by
VocabularyPickerWidget, which previously inherited from the old widget.
    lib/canonical/widgets/popup.py

Fixed lint error.
    lib/canonical/widgets/templates/form-picker.pt

Add ability to send a special error message when no results are returned.
This is necessary for the "Also affects project" link on the bug page.
See how it is used in the widgets/popup.py file.
    lib/canonical/widgets/templates/vocabulary-picker.js

This view has a very customized error message that actually contains
a link instead of telling the user to just click "Choose". Of course,
since it degrades in non-js browsers to a "Find"...

Read more...

Revision history for this message
Martin Albisetti (beuno) wrote :

Hi Edwin,

I'm very happy you're killing the simplepopupwidget, it's a measure of success :)

The one thing that I don't understand about this branch, is why did you have to create a generic icon, when all our objects already have generic icons.
I the example in https://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/3/+distrotask, I would expect to see the generic project icon.

Any technical why that doesn't happen?

review: Needs Information (ui)
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

On Thu, Jul 30, 2009 at 5:58 PM, Martin Albisetti<email address hidden> wrote:
> The one thing that I don't understand about this branch, is why did you have to create a generic icon, when all our objects already have generic icons.
> I the example in https://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/3/+distrotask, I would expect to see the generic project icon.

That form's picker actually shows sourcepackagenames and not projects.
There is a package-source sprite, but that appears to be a different
kind of object. Even if a sprite exists,
ObjectImageDisplayAPI.sprite_css() has to be updated so that we know
which object type maps to which sprite. Until that is all taken care
of, I think the default icon makes it look nicer.

Revision history for this message
Martin Albisetti (beuno) wrote :

> That form's picker actually shows sourcepackagenames and not projects.
> There is a package-source sprite, but that appears to be a different
> kind of object. Even if a sprite exists,
> ObjectImageDisplayAPI.sprite_css() has to be updated so that we know
> which object type maps to which sprite. Until that is all taken care
> of, I think the default icon makes it look nicer.

Aaaah, I see. Sounds reasonable. Could you make sure you file a bug for that?

Revision history for this message
Martin Albisetti (beuno) :
review: Approve (ui)
Revision history for this message
Paul Hummer (rockstar) wrote :

Edwin-

  I'm glad to see these changes landed, especially after working with the
person picker and realizing how sexy it really is. I have a few small comments
to make, but otherwise, lets get this landed! :)

 vote approve
 merge approved

On Thu, 30 Jul 2009 21:25:20 -0000, Edwin Grubbs <email address hidden>
wrote:
> === modified file 'lib/canonical/launchpad/browser/vocabulary.py'
> --- lib/canonical/launchpad/browser/vocabulary.py 2009-07-27
> @@ -87,6 +94,16 @@
> extra.description = branch.bzr_identity
> return extra
>
> +@implementer(IPickerEntry)
> +@adapter(ISourcePackageName)
> +def sourcepackagename_to_pickerentry(sourcepackagename):
> + """Adapts IBranch to IPickerEntry."""
> + extra = default_pickerentry_adapter(sourcepackagename)
> + descriptions = getSourcePackageDescriptions([sourcepackagename])
> + extra.description = descriptions.get(
> + sourcepackagename.name, "Not yet built")
> + return extra
> +
>
> class HugeVocabularyJSONView:
> """Export vocabularies as JSON.

The docstring looks like a simple c-n-p problem. It's actually adapting a
source package to a picker entry.

> @@ -131,8 +148,12 @@
> # The canonical_url without just the path (no hostname) can
> # be passed directly into the REST PATCH call.
> api_request = IWebServiceClientRequest(self.request)
> - entry['api_uri'] = canonical_url(
> - term.value, request=api_request,
> path_only_if_possible=True)
> + try:
> + entry['api_uri'] = canonical_url(
> + term.value, request=api_request,
> + path_only_if_possible=True)
> + except NoCanonicalUrl:
> + entry['api_uri'] = 'Could not find canonical url.'
> picker_entry = IPickerEntry(term.value)
> if picker_entry.description is not None:
> if len(picker_entry.description) > MAX_DESCRIPTION_LENGTH:
>

As discussed in IRC, please add a comment explaining why it's okay to let this
exception continue.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/browser/vocabulary.py'
2--- lib/canonical/launchpad/browser/vocabulary.py 2009-07-27 20:42:29 +0000
3+++ lib/canonical/launchpad/browser/vocabulary.py 2009-07-29 03:29:24 +0000
4@@ -6,10 +6,12 @@
5 __metaclass__ = type
6
7 __all__ = [
8+ 'branch_to_vocabularyjson',
9+ 'default_vocabularyjson_adapter',
10 'HugeVocabularyJSONView',
11 'IPickerEntry',
12 'person_to_vocabularyjson',
13- 'default_vocabularyjson_adapter',
14+ 'sourcepackagename_to_vocabularyjson',
15 ]
16
17 import simplejson
18@@ -26,9 +28,12 @@
19
20 from lp.code.interfaces.branch import IBranch
21 from lp.registry.interfaces.person import IPerson
22+from lp.registry.interfaces.sourcepackagename import ISourcePackageName
23+from lp.registry.model.sourcepackagename import getSourcePackageDescriptions
24
25 from canonical.launchpad.webapp.batching import BatchNavigator
26-from canonical.launchpad.webapp.interfaces import UnexpectedFormData
27+from canonical.launchpad.webapp.interfaces import (
28+ NoCanonicalUrl, UnexpectedFormData)
29 from canonical.launchpad.webapp.publisher import canonical_url
30 from canonical.launchpad.webapp.tales import ObjectImageDisplayAPI
31 from canonical.launchpad.webapp.vocabulary import IHugeVocabulary
32@@ -68,6 +73,8 @@
33 extra.description = obj.summary
34 display_api = ObjectImageDisplayAPI(obj)
35 extra.css = display_api.sprite_css()
36+ if extra.css is None:
37+ extra.css = 'sprite bullet'
38 return extra
39
40 @implementer(IPickerEntry)
41@@ -87,6 +94,16 @@
42 extra.description = branch.bzr_identity
43 return extra
44
45+@implementer(IPickerEntry)
46+@adapter(ISourcePackageName)
47+def sourcepackagename_to_pickerentry(sourcepackagename):
48+ """Adapts IBranch to IPickerEntry."""
49+ extra = default_pickerentry_adapter(sourcepackagename)
50+ descriptions = getSourcePackageDescriptions([sourcepackagename])
51+ extra.description = descriptions.get(
52+ sourcepackagename.name, "Not yet built")
53+ return extra
54+
55
56 class HugeVocabularyJSONView:
57 """Export vocabularies as JSON.
58@@ -131,8 +148,12 @@
59 # The canonical_url without just the path (no hostname) can
60 # be passed directly into the REST PATCH call.
61 api_request = IWebServiceClientRequest(self.request)
62- entry['api_uri'] = canonical_url(
63- term.value, request=api_request, path_only_if_possible=True)
64+ try:
65+ entry['api_uri'] = canonical_url(
66+ term.value, request=api_request,
67+ path_only_if_possible=True)
68+ except NoCanonicalUrl:
69+ entry['api_uri'] = 'Could not find canonical url.'
70 picker_entry = IPickerEntry(term.value)
71 if picker_entry.description is not None:
72 if len(picker_entry.description) > MAX_DESCRIPTION_LENGTH:
73
74=== removed file 'lib/canonical/launchpad/doc/popup-view.txt'
75--- lib/canonical/launchpad/doc/popup-view.txt 2009-05-01 19:48:19 +0000
76+++ lib/canonical/launchpad/doc/popup-view.txt 1970-01-01 00:00:00 +0000
77@@ -1,129 +0,0 @@
78-= The SinglePopupView =
79-
80-This is the view used by the page rendered inside the popup windows we use
81-for fields which have an IHugeVocabulary.
82-
83- >>> from canonical.launchpad.interfaces import IBugTaskSet
84- >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
85-
86- >>> bugtask = getUtility(IBugTaskSet).get(2)
87- >>> form = dict(vocabulary='Product', field='field.product')
88- >>> view = create_view(bugtask, 'popup-window', form=form)
89- >>> view.title()
90- 'Select a project'
91- >>> view.vocabulary()
92- <lp.registry.vocabularies.ProductVocabulary...
93-
94-We didn't provide any search terms, so there will be no items to display.
95-
96- >>> batch = view.search()
97- >>> print len(batch.batch)
98- 0
99-
100-If we do provide some search text, though, we'll see all items that match
101-our search.
102-
103- >>> form['search'] = 'firefox'
104- >>> view = create_view(bugtask, 'popup-window', form=form)
105- >>> batch = view.search()
106- >>> print len(batch.batch)
107- 1
108- >>> [item.title for item in batch.batch]
109- [u'Mozilla Firefox']
110-
111-== The SearchForUpstreamPopupView ==
112-
113-This is a specialized version of SinglePopupView which also includes a link
114-for the user to register a new Product, in case the one he's looking for is
115-not yet registered. Since this page opens in a popup window, this link's
116-target will be the parent window. Also, this link is only shown when a
117-non-empty string was used for the search.
118-
119- >>> form['search'] = ''
120- >>> view = create_view(bugtask, 'popup-search-upstream', form=form)
121- >>> batch = view.search()
122- >>> view.extra_bottom
123- ''
124-
125- >>> form['search'] = 'fooo'
126- >>> view = create_view(bugtask, 'popup-search-upstream', form=form)
127- >>> batch = view.search()
128- >>> print view.extra_bottom
129- Didn't find the project you were looking for?
130- <a href="http://bugs.launchpad.dev/firefox/+bug/1/+affects-new-product"
131- target="_parent">Register it</a>.
132-
133-== Error handling ==
134-
135-There are a few situations which we need to be careful about.
136-
137-1. When somebody hits that page directly, without any form arguments:
138-
139- >>> from canonical.launchpad.webapp.publisher import rootObject
140- >>> from canonical.widgets.popup import SinglePopupView
141- >>> request = LaunchpadTestRequest(
142- ... SERVER_URL='http://127.0.0.1/@@popup-window', form={})
143- >>> SinglePopupView(rootObject, request)
144- Traceback (most recent call last):
145- ...
146- NotFound: ...
147-
148-2. When someone tries using that page for a vocabulary which doesn't
149-implement IHugeVocabulary.
150-
151- >>> form = {
152- ... 'vocabulary': 'Distribution', 'field':'field.distribution'}
153- >>> request = LaunchpadTestRequest(
154- ... SERVER_URL='http://127.0.0.1/@@popup-window', form=form)
155- >>> popup_view = SinglePopupView(rootObject, request)
156- >>> popup_view.vocabulary()
157- Traceback (most recent call last):
158- ...
159- UnexpectedFormData: Non-huge vocabulary Distribution
160-
161-3. When someone specifies an unknown vocabulary name.
162-
163- >>> form = {
164- ... 'vocabulary': 'FooBar', 'field':'field.distribution'}
165- >>> request = LaunchpadTestRequest(
166- ... SERVER_URL='http://127.0.0.1/@@popup-window', form=form)
167- >>> popup_view = SinglePopupView(rootObject, request)
168- >>> popup_view.vocabulary()
169- Traceback (most recent call last):
170- ...
171- UnexpectedFormData: Unknown vocabulary FooBar
172-
173-4. When someone specifies an empty vocabulary name.
174-
175- >>> form = {
176- ... 'vocabulary': '', 'field':'field.distribution'}
177- >>> request = LaunchpadTestRequest(
178- ... SERVER_URL='http://127.0.0.1/@@popup-window', form=form)
179- >>> popup_view = SinglePopupView(rootObject, request)
180- >>> popup_view.vocabulary()
181- Traceback (most recent call last):
182- ...
183- UnexpectedFormData: No vocabulary specified
184-
185-
186-== Sanitation ==
187-
188-The `field` parameter provided as one of the GET parameters for the popup
189-view is passed into javascript code in the page, so we must make sure that
190-it's sanitised.
191-
192- >>> form = dict(vocabulary='Product', field='field.product\'/alert("xss rules")/alert(document.cookie)/')
193- >>> view = create_view(bugtask, 'popup-window', form=form)
194-
195-The field attribute used by the view provides a javascript encoded
196-representation of the field parameter.
197-
198- # XXX AaronBentley 2009-02-16 bug=330243: This fails on Intrepid,
199- # apparently due to differences in the JS compressor.
200- #>>> print view.field
201- "field.product'\/alert(\"xss rules\")\/alert(document.cookie)\/"
202-
203- #>>> from BeautifulSoup import BeautifulSoup
204- #>>> soup = BeautifulSoup(view())
205- #>>> soup.find('script')
206- #<script language="Javascript" type="text/javascript">field = "field.product'\/alert(\"xss rules\")\/alert(document.cookie)\/";</script>
207
208=== removed file 'lib/canonical/launchpad/doc/popup-widget.txt'
209--- lib/canonical/launchpad/doc/popup-widget.txt 2007-11-15 18:43:55 +0000
210+++ lib/canonical/launchpad/doc/popup-widget.txt 1970-01-01 00:00:00 +0000
211@@ -1,128 +0,0 @@
212-= The SinglePopupWidget =
213-
214-There's a widget, SinglePopupWidget, for selecting a single value from a
215-huge vocabulary. It provides a simple text input line, and a popup
216-window, where you can search for valid values.
217-
218- >>> from canonical.widgets.popup import SinglePopupWidget
219-
220-In order to show how it works, we need a field, which uses a huge
221-vocabulary. We also need to bind it to a context for things to work:
222-
223- >>> from canonical.launchpad.interfaces import (
224- ... ITeamReassignment, IPersonSet)
225- >>> owner_field = ITeamReassignment['owner']
226- >>> admins = getUtility(IPersonSet).getByName('admins')
227- >>> owner_field = owner_field.bind(admins)
228-
229-We also need a request before we can initialise the widget:
230-
231- >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
232- >>> popup_widget = SinglePopupWidget(
233- ... owner_field, owner_field.vocabulary, LaunchpadTestRequest())
234- >>> print popup_widget()
235- <input type="text" value="" id="field.owner"
236- name="field.owner" size="20"
237- maxlength=""
238- onKeyPress="" style=""
239- class="" />
240- <BLANKLINE>
241- (<a href="...@@popup-window?vocabulary=ValidTeamOwner...
242- <BLANKLINE>
243- <iframe style="display: none" id="popup_iframe_field.owner"...
244- <BLANKLINE>
245-
246-Now, since we didn't pass a value to the form, it has no input, and
247-getInputValue() fails:
248-
249- >>> popup_widget.hasInput()
250- False
251- >>> popup_widget.getInputValue()
252- Traceback (most recent call last):
253- ...
254- MissingInputError: ('field.owner', u'Owner', None)
255-
256-Let's supply a nonexistent value to the form:
257-
258- >>> form = {'field.owner': u'non-existant-value'}
259- >>> popup_widget = SinglePopupWidget(
260- ... owner_field, owner_field.vocabulary,
261- ... LaunchpadTestRequest(form=form))
262-
263-Now hasInput() returns that there is a input value, but getInputValue()
264-still fails:
265-
266- >>> popup_widget.hasInput()
267- True
268- >>> popup_widget.getInputValue()
269- Traceback (most recent call last):
270- ...
271- ConversionError: ('Invalid value',
272- token u'non-existant-value' not found in vocabulary)
273-
274-If we supply a token that is in the vocabulary, everything works as
275-expected:
276-
277- >>> form = {'field.owner': u'shipit-admins'}
278- >>> popup_widget = SinglePopupWidget(
279- ... owner_field, owner_field.vocabulary,
280- ... LaunchpadTestRequest(form=form))
281- >>> popup_widget.hasInput()
282- True
283- >>> shipit_admins = popup_widget.getInputValue()
284- >>> shipit_admins.displayname
285- u'ShipIt Administrators'
286-
287-If we submit a string that doesn't match an exact token, but the
288-vocabulary search returns a few matches, we don't have a valid input:
289-
290- >>> form = {'field.owner': u'shipit'}
291- >>> popup_widget = SinglePopupWidget(
292- ... owner_field, owner_field.vocabulary,
293- ... LaunchpadTestRequest(form=form))
294- >>> popup_widget.hasInput()
295- True
296- >>> shipit_admins = popup_widget.getInputValue()
297- Traceback (most recent call last):
298- ...
299- ConversionError: ('Invalid value',
300- token u'shipit' not found in vocabulary)
301-
302-
303-== The SearchForUpstreamPopupWidget ==
304-
305-This is a specialized version of SinglePopupWidget whose 'Choose' link opens
306-a different page (popup-search-upstream) and is used when searching for an
307-upstream that is also affected by a given bug. The page linked from the
308-widget includes a link which allows the user to register the upstream if
309-it doesn't exist.
310-
311- >>> from canonical.widgets import SearchForUpstreamPopupWidget
312- >>> from canonical.launchpad.interfaces import (
313- ... IAddBugTaskForm, IBugSet)
314- >>> product_field = IAddBugTaskForm['product']
315- >>> bug = getUtility(IBugSet).get(1)
316- >>> product_field = product_field.bind(bug)
317-
318- >>> popup_widget = SearchForUpstreamPopupWidget(
319- ... product_field, product_field.vocabulary, LaunchpadTestRequest())
320- >>> print popup_widget()
321- <input type="text" value="" id="field.product"...
322- <BLANKLINE>
323- (<a href="javascript:popup_window('@@popup-search-upstream...
324- <BLANKLINE>
325- ...
326-
327-
328-== Escaping HTML entities ==
329-
330-Just in case someone tries to XSS us.
331-
332- >>> form = {'field.owner': u'"><script>alert(Y0u @r3 0wn3d!!!);</script>'}
333- >>> popup_widget = SinglePopupWidget(
334- ... owner_field, owner_field.vocabulary,
335- ... LaunchpadTestRequest(form=form))
336- >>> print popup_widget()
337- <input type="text" value="&quot;&gt;&lt;script&gt;alert(Y0u
338- @r3 0wn3d!!!);&lt;/script&gt;" id="field.owner"...
339-
340
341=== modified file 'lib/canonical/launchpad/doc/project-scope-widget.txt'
342--- lib/canonical/launchpad/doc/project-scope-widget.txt 2009-07-24 15:32:18 +0000
343+++ lib/canonical/launchpad/doc/project-scope-widget.txt 2009-07-30 17:58:16 +0000
344@@ -104,15 +104,6 @@
345 >>> selected_scope.name
346 u'mozilla'
347
348-The project is actually selected using a contained widget associated
349-with the field's vocabulary. In this case, it is a VocabularyPickerWidget
350-which offers a link to Choose the proper project:
351-
352- >>> widget.target_widget
353- <...VocabularyPickerWidget...>
354- >>> print widget.target_widget.popupHref()
355- javascript:popup_window('@@popup-vocabulary-picker?vocabulary=Project&...
356-
357 If an non-existant distribution name is provided, a widget error is
358 raised:
359
360
361=== modified file 'lib/canonical/launchpad/doc/vocabularies.txt'
362--- lib/canonical/launchpad/doc/vocabularies.txt 2009-07-28 13:48:07 +0000
363+++ lib/canonical/launchpad/doc/vocabularies.txt 2009-07-29 16:10:53 +0000
364@@ -338,14 +338,13 @@
365 >>> len(spn_terms)
366 2
367 >>> [(term.token, term.title) for term in spn_terms]
368- [('mozilla', 'Not yet built'),
369- ('mozilla-firefox', u'Source of: mozilla-firefox, mozilla-firefox-data')]
370+ [('mozilla', u'mozilla'), ('mozilla-firefox', u'mozilla-firefox')]
371
372 >>> spn_terms = spn_vocabulary.searchForTerms("pmount")
373 >>> len(spn_terms)
374 1
375 >>> [(term.token, term.title) for term in spn_terms]
376- [('pmount', u'Source of: pmount')]
377+ [('pmount', u'pmount')]
378
379
380 === BranchVocabulary ===
381
382=== removed file 'lib/canonical/launchpad/pagetests/standalone/xx-show-people-with-password-only.txt'
383--- lib/canonical/launchpad/pagetests/standalone/xx-show-people-with-password-only.txt 2006-11-29 13:24:27 +0000
384+++ lib/canonical/launchpad/pagetests/standalone/xx-show-people-with-password-only.txt 1970-01-01 00:00:00 +0000
385@@ -1,14 +0,0 @@
386-Get the ValidPersonOrTeams vocabulary popup, requesting all valid people and
387-teams with 'person' as part of their names or email addresses.
388-
389- >>> print http(r"""
390- ... GET /firefox/+bug/1/+editstatus/@@popup-window?search=person&vocabulary=ValidPersonOrTeam&field=foo HTTP/1.1
391- ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
392- ... Cookie: __ac_name="admin"
393- ... """)
394- HTTP/1.1 200 Ok
395- ...
396- ...No Privileges Person...
397- ...Sample Person...
398- ...
399-
400
401=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
402--- lib/canonical/launchpad/webapp/configure.zcml 2009-07-23 19:58:30 +0000
403+++ lib/canonical/launchpad/webapp/configure.zcml 2009-07-29 21:22:38 +0000
404@@ -752,7 +752,17 @@
405 permission="zope.Public"
406 />
407
408- <!-- Define the widget used by Choice fields that use huge vocabularies -->
409+ <!-- Define the widget used by PublicPersonChoice fields. -->
410+ <view
411+ type="zope.publisher.interfaces.browser.IBrowserRequest"
412+ for="canonical.launchpad.fields.PublicPersonChoice
413+ canonical.launchpad.webapp.vocabulary.IHugeVocabulary"
414+ provides="zope.app.form.interfaces.IInputWidget"
415+ factory="canonical.widgets.popup.PersonPickerWidget"
416+ permission="zope.Public"
417+ />
418+
419+ <!-- Define the widget used by fields that use BranchVocabularyBase. -->
420 <view
421 type="zope.publisher.interfaces.browser.IBrowserRequest"
422 for="zope.schema.interfaces.IChoice
423
424=== modified file 'lib/canonical/launchpad/windmill/testing/widgets.py'
425--- lib/canonical/launchpad/windmill/testing/widgets.py 2009-07-17 00:26:05 +0000
426+++ lib/canonical/launchpad/windmill/testing/widgets.py 2009-07-30 19:03:57 +0000
427@@ -4,7 +4,14 @@
428 """Test helpers for common AJAX widgets."""
429
430 __metaclass__ = type
431-__all__ = []
432+__all__ = [
433+ 'FormPickerWidgetTest',
434+ 'InlineEditorWidgetTest',
435+ 'InlinePickerWidgetButtonTest',
436+ 'InlinePickerWidgetSearchTest',
437+ 'search_and_select_picker_widget',
438+ 'search_picker_widget',
439+ ]
440
441
442 from windmill.authoring import WindmillTestClient
443@@ -85,9 +92,8 @@
444 xpath=widget_base + '/span[1]', validator=self.new_value)
445
446
447-def _search_picker_widget(client, search_text, result_index):
448- """Search in picker widget and select an item."""
449- # Search for search_text in picker widget.
450+def search_picker_widget(client, search_text):
451+ """Search in picker widget."""
452 search_box_xpath = (u"//table[contains(@class, 'yui-picker') "
453 "and not(contains(@class, 'yui-picker-hidden'))]"
454 "//input[@class='yui-picker-search']")
455@@ -99,6 +105,10 @@
456 xpath=u"//table[contains(@class, 'yui-picker') "
457 "and not(contains(@class, 'yui-picker-hidden'))]"
458 "//div[@class='yui-picker-search-box']/button")
459+
460+def search_and_select_picker_widget(client, search_text, result_index):
461+ """Search in picker widget and select item."""
462+ search_picker_widget(client, search_text)
463 # Select item at the result_index in the list.
464 item_xpath = (u"//table[contains(@class, 'yui-picker') "
465 "and not(contains(@class, 'yui-picker-hidden'))]"
466@@ -157,8 +167,8 @@
467 client.click(xpath=button_xpath)
468
469 # Search picker.
470- _search_picker_widget(client, self.search_text,
471- self.result_index)
472+ search_and_select_picker_widget(
473+ client, self.search_text, self.result_index)
474
475 # Verify update.
476 client.waits.sleep(milliseconds=u'2000')
477@@ -297,7 +307,8 @@
478 client.click(id=self.choose_link_id)
479
480 # Search picker.
481- _search_picker_widget(client, self.search_text, self.result_index)
482+ search_and_select_picker_widget(
483+ client, self.search_text, self.result_index)
484
485 # Verify value.
486 client.asserts.assertProperty(
487
488=== modified file 'lib/canonical/launchpad/zcml/launchpad.zcml'
489--- lib/canonical/launchpad/zcml/launchpad.zcml 2009-07-24 20:44:12 +0000
490+++ lib/canonical/launchpad/zcml/launchpad.zcml 2009-07-29 03:29:24 +0000
491@@ -150,6 +150,10 @@
492 factory="canonical.launchpad.browser.vocabulary.branch_to_pickerentry"
493 />
494
495+ <adapter
496+ factory="canonical.launchpad.browser.vocabulary.sourcepackagename_to_pickerentry"
497+ />
498+
499 <facet facet="overview">
500
501 <utility
502
503=== modified file 'lib/canonical/widgets/__init__.py'
504--- lib/canonical/widgets/__init__.py 2009-06-25 05:39:50 +0000
505+++ lib/canonical/widgets/__init__.py 2009-07-29 03:29:24 +0000
506@@ -18,8 +18,6 @@
507 from canonical.widgets.itemswidgets import *
508 from canonical.widgets.location import LocationWidget
509 from canonical.widgets.owner import IUserWidget, HiddenUserWidget
510-from canonical.widgets.popup import (
511- ISinglePopupWidget, SearchForUpstreamPopupWidget, SinglePopupWidget)
512 from canonical.widgets.password import PasswordChangeWidget
513 from canonical.widgets.textwidgets import (
514 DelimitedListWidget, LocalDateTimeWidget, LowerCaseTextWidget,
515
516=== modified file 'lib/canonical/widgets/bugtask.py'
517--- lib/canonical/widgets/bugtask.py 2009-07-17 00:26:05 +0000
518+++ lib/canonical/widgets/bugtask.py 2009-07-29 03:29:24 +0000
519@@ -30,7 +30,7 @@
520 from canonical.launchpad.webapp.tales import TeamFormatterAPI
521 from canonical.widgets.helpers import get_widget_template
522 from canonical.widgets.itemswidgets import LaunchpadRadioWidget
523-from canonical.widgets.popup import SinglePopupWidget
524+from canonical.widgets.popup import VocabularyPickerWidget
525 from canonical.widgets.textwidgets import StrippedTextWidget, URIWidget
526
527 class BugTaskAssigneeWidget(Widget):
528@@ -51,7 +51,7 @@
529 #
530 # See zope.app.form.interfaces.IInputWidget.
531 self.required = False
532- self.assignee_chooser_widget = SinglePopupWidget(
533+ self.assignee_chooser_widget = VocabularyPickerWidget(
534 context, context.vocabulary, request)
535 self.setUpNames()
536
537@@ -419,7 +419,7 @@
538 contents='\n'.join(rendered_items))
539
540
541-class BugTaskSourcePackageNameWidget(SinglePopupWidget):
542+class BugTaskSourcePackageNameWidget(VocabularyPickerWidget):
543 """A widget for associating a bugtask with a SourcePackageName.
544
545 It accepts both binary and source package names.
546
547=== modified file 'lib/canonical/widgets/configure.zcml'
548--- lib/canonical/widgets/configure.zcml 2009-07-13 18:15:02 +0000
549+++ lib/canonical/widgets/configure.zcml 2009-07-29 03:29:24 +0000
550@@ -6,33 +6,6 @@
551 xmlns="http://namespaces.zope.org/zope"
552 xmlns:browser="http://namespaces.zope.org/browser">
553
554- <!-- XXX: StuartBishop 2004-11-12: This stanza might not be necessary. -->
555- <class class="canonical.widgets.SinglePopupWidget">
556- <allow interface="canonical.widgets.ISinglePopupWidget" />
557- </class>
558-
559- <!-- Define the views needed by the popup widget
560- XXX: StuartBishop 2004-11-12:
561- These should probably just be attached to the add and edit forms.
562- -->
563- <browser:page
564- for="*"
565- class="canonical.widgets.popup.SinglePopupView"
566- allowed_interface="canonical.widgets.popup.ISinglePopupView"
567- permission="zope.Public"
568- name="popup-window"
569- template="templates/popup-window.pt"
570- />
571-
572- <browser:page
573- for="*"
574- class="canonical.widgets.popup.SearchForUpstreamPopupView"
575- allowed_interface="canonical.widgets.popup.ISinglePopupView"
576- permission="zope.Public"
577- name="popup-search-upstream"
578- template="templates/popup-window.pt"
579- />
580-
581 <!--
582 XXX: anonymous 2004-08-14: Feed back into Zope3.
583 The Zope3 WidgetInputError does not handle lists of validation
584
585=== modified file 'lib/canonical/widgets/popup.py'
586--- lib/canonical/widgets/popup.py 2009-07-27 20:18:30 +0000
587+++ lib/canonical/widgets/popup.py 2009-07-30 17:58:16 +0000
588@@ -11,69 +11,34 @@
589 import cgi
590 import simplejson
591
592-from zope.interface import Attribute, implements, Interface
593-from zope.component import getUtility
594-from zope.schema import TextLine
595 from zope.schema.interfaces import IChoice
596-from zope.app.form.browser.interfaces import ISimpleInputWidget
597 from zope.app.form.browser.itemswidgets import (
598 ItemsWidgetBase, SingleDataHelper)
599-from zope.app.schema.vocabulary import IVocabularyFactory
600-from zope.publisher.interfaces import NotFound
601-from zope.component.interfaces import ComponentLookupError
602
603 from z3c.ptcompat import ViewPageTemplateFile
604
605 from canonical.launchpad.webapp import canonical_url
606-from canonical.launchpad.webapp.batching import BatchNavigator
607-from canonical.launchpad.webapp.vocabulary import IHugeVocabulary
608-from canonical.launchpad.interfaces import UnexpectedFormData
609 from canonical.cachedproperty import cachedproperty
610
611
612-class ISinglePopupWidget(ISimpleInputWidget):
613- # I chose to use onKeyPress because onChange only fires when focus
614- # leaves the element, and that's very inconvenient.
615- onKeyPress = Attribute('''Optional javascript code to be executed
616- as text in input is changed''')
617- cssClass = Attribute('''CSS class to be assigned to the input widget''')
618- style = Attribute('''CSS style to be applied to the input widget''')
619- popup_name = TextLine(
620- title=u'The name our popup page is registered with.')
621- def formToken():
622- 'The token representing the value to display, possibly invalid'
623- def chooseLink():
624- 'The HTML link text and inline frame for the Choose.. link.'
625- def inputField():
626- 'The HTML for the form input that is linked to this popup'
627- def popupHref():
628- 'The contents to go into the href tag used to popup the select window'
629- def matches():
630- """List of tokens matching the current input.
631-
632- An empty list should be returned if 'too many' results are found.
633- """
634-
635-
636-class SinglePopupWidget(SingleDataHelper, ItemsWidgetBase):
637- """Window popup widget for single item choices from a huge vocabulary.
638-
639- The huge vocabulary must be registered by name in the vocabulary registry.
640- """
641- implements(ISinglePopupWidget)
642-
643- # ZPT that renders our widget
644-
645- __call__ = ViewPageTemplateFile('templates/popup.pt')
646-
647- default = ''
648-
649+class VocabularyPickerWidget(SingleDataHelper, ItemsWidgetBase):
650+ """Wrapper for the lazr-js picker/picker.js widget."""
651+
652+ __call__ = ViewPageTemplateFile('templates/form-picker.pt')
653+
654+ popup_name = 'popup-vocabulary-picker'
655+
656+ # Override inherited attributes for the form field.
657 displayWidth = '20'
658 displayMaxWidth = ''
659+ default = ''
660 onKeyPress = ''
661 style = ''
662 cssClass = ''
663- popup_name = 'popup-window'
664+
665+ step_title = 'Search'
666+ # Defaults to self.vocabulary.displayname.
667+ header = None
668
669 @cachedproperty
670 def matches(self):
671@@ -81,7 +46,7 @@
672 user currently has entered in the form.
673 """
674 # Pull form value using the parent class to avoid loop
675- formValue = super(SinglePopupWidget, self)._getFormInput()
676+ formValue = super(VocabularyPickerWidget, self)._getFormInput()
677 if not formValue:
678 return []
679
680@@ -126,146 +91,6 @@
681 maxlength="%(displayMaxWidth)s"
682 onKeyPress="%(onKeyPress)s" style="%(style)s"
683 class="%(cssClass)s" />""" % d
684-
685- def chooseLink(self):
686- return """(<a href="%s" class="js-action">Choose&hellip;</a>)
687-
688- <iframe style="display: none"
689- id="popup_iframe_%s"
690- src="javascript:void(0);"
691- name="popup_iframe_%s"></iframe>
692- """ % (self.popupHref(), self.name, self.name)
693-
694- def popupHref(self):
695- template = (
696- "javascript:"
697- "popup_window('@@%s?"
698- "vocabulary=%s&field=%s&search="
699- "'+escape(document.getElementById('%s').value),"
700- "'%s','300','420')"
701- ) % (self.popup_name, self.context.vocabularyName, self.name,
702- self.name, self.name)
703- if self.onKeyPress:
704- # XXX kiko 2005-09-27: I suspect onkeypress() here is
705- # non-standard, but it works for me, and enough researching for
706- # tonight. It may be better to use dispatchEvent or a
707- # compatibility function
708- template += ("; document.getElementById('%s').onkeypress()" %
709- self.name)
710- return template
711-
712-
713-class ISinglePopupView(Interface):
714-
715- batch = Attribute('The BatchNavigator of the current results to display')
716- page_name = TextLine(title=u'The name this page is registered with.')
717-
718- def title():
719- """Title to use on the popup page"""
720-
721- def vocabulary():
722- """Return the IHugeVocabulary to display in the popup window."""
723-
724- def search():
725- """Return the BatchNavigator of the current terms to display."""
726-
727- def hasMoreThanOnePage(self):
728- """Return True if there's more than one page with results."""
729-
730- field = Attribute("The field parameter, sanitized.")
731-
732-
733-class SinglePopupView(object):
734- implements(ISinglePopupView)
735-
736- _batchsize = 10
737- batch = None
738- page_name = 'popup-window'
739-
740- def __init__(self, context, request):
741- if ("vocabulary" not in request.form or
742- "field" not in request.form):
743- # Hand-hacked URLs get no love from us
744- raise NotFound(self, "/@@popup-window", request)
745- self.context = context
746- self.request = request
747-
748- def title(self):
749- """See ISinglePopupView"""
750- return self.vocabulary().displayname
751-
752- def vocabulary(self):
753- """See ISinglePopupView"""
754- vocabulary_name = self.request.form_ng.getOne('vocabulary')
755- if not vocabulary_name:
756- raise UnexpectedFormData('No vocabulary specified')
757- try:
758- factory = getUtility(IVocabularyFactory, vocabulary_name)
759- except ComponentLookupError:
760- # Couldn't find the vocabulary? Adios!
761- raise UnexpectedFormData(
762- 'Unknown vocabulary %s' % vocabulary_name)
763-
764- vocabulary = factory(self.context)
765-
766- if not IHugeVocabulary.providedBy(vocabulary):
767- raise UnexpectedFormData(
768- 'Non-huge vocabulary %s' % vocabulary_name)
769-
770- return vocabulary
771-
772- def search(self):
773- """See ISinglePopupView"""
774- search_text = self.request.get('search', None)
775- self.batch = BatchNavigator(
776- self.vocabulary().searchForTerms(search_text), self.request,
777- size=self._batchsize)
778- return self.batch
779-
780- def hasMoreThanOnePage(self):
781- """See ISinglePopupView"""
782- return len(self.batch.batchPageURLs()) > 1
783-
784- @property
785- def field(self):
786- """See ISinglePopupView"""
787- return simplejson.dumps(self.request.form.get('field', None))
788-
789-
790-class SearchForUpstreamPopupWidget(SinglePopupWidget):
791- """A SinglePopupWidget whose 'Choose' link opens a different page.
792-
793- This widget is used only when searching for an upstream that is also
794- affected by a given bug as the page it links to includes a link which
795- allows the user to register the upstream if it doesn't exist.
796- """
797- popup_name = 'popup-search-upstream'
798-
799-
800-class SearchForUpstreamPopupView(SinglePopupView):
801-
802- page_name = 'popup-search-upstream'
803-
804- @property
805- def extra_bottom(self):
806- search_text = self.request.get('search')
807- if not search_text:
808- return ''
809- return ("Didn't find the project you were looking for? "
810- '<a href="%s/+affects-new-product" target="_parent">'
811- 'Register it</a>.' % canonical_url(self.context))
812-
813-
814-class VocabularyPickerWidget(SinglePopupWidget):
815- """Wrapper for the lazr-js picker/picker.js widget."""
816-
817- popup_name = 'popup-vocabulary-picker'
818-
819- # Defaults to self.vocabulary.displayname.
820- header = None
821-
822- step_title = 'Search'
823-
824 @property
825 def suffix(self):
826 return self.name.replace('.', '-')
827@@ -275,6 +100,17 @@
828 return 'show-widget-%s' % self.suffix
829
830 @property
831+ def extra_no_results_message(self):
832+ """Extra message when there are no results.
833+
834+ Override this in subclasses.
835+
836+ :return: A string that will be passed to Y.Node.create()
837+ so it needs to be contained in a single HTML element.
838+ """
839+ return None
840+
841+ @property
842 def vocabulary_name(self):
843 """The name of the field's vocabulary."""
844 choice = IChoice(self.context)
845@@ -299,18 +135,34 @@
846 else:
847 header = self.header
848
849- js = js_template % dict(
850+ args = dict(
851 vocabulary=self.vocabulary_name,
852 header=header,
853 step_title=self.step_title,
854 show_widget_id=self.show_widget_id,
855- input_id=self.name)
856+ input_id=self.name,
857+ extra_no_results_message=self.extra_no_results_message)
858+ js = js_template % simplejson.dumps(args)
859 # If the YUI widget or javascript is not supported in the browser,
860 # it will degrade to being this "Find..." link instead of the
861- # "Choose..." link.
862- return ('(<a id="%s" href="/people/">'
863- 'Find&hellip;</a>)'
864- '\n<script>\n%s\n</script>') % (self.show_widget_id, js)
865+ # "Choose..." link. This only works if a non-AJAX form is available
866+ # for the field's vocabulary.
867+ if self.nonajax_uri is None:
868+ css = 'unseen'
869+ else:
870+ css = ''
871+ return ('<span class="%s">(<a id="%s" href="/people/">'
872+ 'Find&hellip;</a>)</span>'
873+ '\n<script>\n%s\n</script>'
874+ ) % (css, self.show_widget_id, js)
875+
876+ @property
877+ def nonajax_uri(self):
878+ """Override in subclass to specify a non-AJAX URI for the Find link.
879+
880+ If None is returned, the find link will be hidden.
881+ """
882+ return None
883
884
885 class PersonPickerWidget(VocabularyPickerWidget):
886@@ -322,3 +174,22 @@
887 link += ('or (<a href="/people/+newteam">'
888 'Create a new team&hellip;</a>)')
889 return link
890+
891+ @property
892+ def nonajax_uri(self):
893+ return '/people/'
894+
895+
896+class SearchForUpstreamPopupWidget(VocabularyPickerWidget):
897+ """A SinglePopupWidget with a custom error message.
898+
899+ This widget is used only when searching for an upstream that is also
900+ affected by a given bug as the page it links to includes a link which
901+ allows the user to register the upstream if it doesn't exist.
902+ """
903+
904+ @property
905+ def extra_no_results_message(self):
906+ return ("<strong>Didn't find the project you were looking for? "
907+ '<a href="%s/+affects-new-product">Register it</a>.</strong>'
908+ % canonical_url(self.context.context))
909
910=== modified file 'lib/canonical/widgets/project.py'
911--- lib/canonical/widgets/project.py 2009-06-25 05:30:52 +0000
912+++ lib/canonical/widgets/project.py 2009-07-29 03:29:24 +0000
913@@ -35,7 +35,7 @@
914 # field since it determines the valid target types.
915 # XXX flacoste 2007-02-21 bug=86861: We must
916 # use field.vocabularyName instead of the vocabulary parameter
917- # otherwise SinglePopupWidget will fail.
918+ # otherwise VocabularyPickerWidget will fail.
919 target_field = Choice(
920 __name__='target', title=field.title,
921 description=field.description, vocabulary=field.vocabularyName,
922
923=== renamed file 'lib/canonical/widgets/templates/popup.pt' => 'lib/canonical/widgets/templates/form-picker.pt'
924--- lib/canonical/widgets/templates/popup.pt 2009-07-17 17:59:07 +0000
925+++ lib/canonical/widgets/templates/form-picker.pt 2009-07-30 19:03:57 +0000
926@@ -1,12 +1,11 @@
927+<tal:root xmlns:tal="http://xml.zope.org/namespaces/tal"
928+ omit-tag="">
929 <tal:input replace="structure view/inputField" />
930
931 <tal:search_results tal:condition="not: view/hasValidInput">
932- <select
933- tal:define="matches view/matches"
934- tal:condition="python:len(matches) > 0"
935- >
936+ <select tal:condition="view/matches">
937 <option
938- tal:repeat="match matches"
939+ tal:repeat="match view/matches"
940 tal:attributes="value match/token;
941 selected python:path('match/token') == path('view/formToken');
942 onclick string:this.form['${view/name}'].value = this.value"
943@@ -16,4 +15,4 @@
944 </tal:search_results>
945
946 <tal:chooseLink replace="structure view/chooseLink" />
947-
948+</tal:root>
949
950=== removed file 'lib/canonical/widgets/templates/popup-window.pt'
951--- lib/canonical/widgets/templates/popup-window.pt 2009-07-17 17:59:07 +0000
952+++ lib/canonical/widgets/templates/popup-window.pt 1970-01-01 00:00:00 +0000
953@@ -1,104 +0,0 @@
954-<html
955- xmlns:tal="http://xml.zope.org/namespaces/tal"
956- xmlns:metal="http://xml.zope.org/namespaces/metal"
957- xmlns:i18n="http://xml.zope.org/namespaces/i18n"
958- >
959- <head>
960- <style type="text/css">
961- <!--
962- body {
963- font-size: 80%;
964- }
965- h1 {
966- font-size: 95%;
967- display: block;
968- background: #e0e0e0;
969- padding: 0.5em;
970- }
971- -->
972- </style>
973- <script language="Javascript" type="text/javascript"
974- tal:content="string:field = ${view/field};" />
975- <script language="Javascript" type="text/javascript">
976-function update(value) {
977- e = window.parent.document.getElementById(field);
978- if (e == null) {
979- alert(field);
980- }
981- if (e != null) {
982- e.value = value;
983- e.focus();
984- hide();
985- }
986-}
987-
988-function hide() {
989- parent_doc = window.parent.document
990- iframe = parent_doc.getElementById('popup_iframe_' + field);
991- // XXX: kiko 2007-03-12:
992- // when we alter the iframe's display value, the
993- // @@popup-window page is reloaded for some reason, and
994- // I can't find out why. I've tried
995- // event.preventBubble() and return false.
996- iframe.style.display = 'none';
997- return false;
998-}
999- </script>
1000- <title tal:content="view/title">Title</title>
1001- </head>
1002- <body onLoad="document.searchform.search.focus()">
1003- <div style="float: right; padding: 0.5em;">
1004- <a href="#"
1005- style="text-decoration: none;" onClick="javascript: hide()">
1006- <small>[cancel]</small></a></div>
1007- <div tal:define="batchnav view/search">
1008- <h1 tal:content="view/title">Title</h1>
1009- <form method="get" name="searchform" style="text-align: center">
1010- <input type="text" name="search"
1011- tal:attributes="value request/search|nothing" />
1012- <input type="submit" value="Search" />
1013- <input type="hidden" name="vocabulary"
1014- tal:attributes="value request/vocabulary" />
1015- <input type="hidden" name="field"
1016- tal:attributes="value request/field" />
1017- </form>
1018-
1019- <table tal:condition="view/hasMoreThanOnePage">
1020- <tr>
1021- <th>
1022- <a tal:attributes="href batchnav/prevBatchURL">&larr;</a>
1023- </th>
1024- <tal:x repeat="page_link batchnav/batchPageURLs">
1025- <th tal:condition="python:not
1026- str(page_link.keys()[0]).startswith('_')">
1027- <a tal:attributes="href
1028- python:page_link.values()[0]"
1029- tal:content="python:page_link.keys()[0]">#</a>
1030- </th>
1031- </tal:x>
1032- <th>
1033- <a tal:attributes="href batchnav/nextBatchURL">&rarr;</a>
1034- </th>
1035- </tr>
1036- </table>
1037- <dl>
1038- <tal:x repeat="item batchnav/currentBatch">
1039- <dt><img src="/@@/bullet" alt="" border="" />
1040- <a href="#" title="Click to select"
1041- tal:content="item/token"
1042- tal:attributes="onclick
1043- string:update('${item/token}')">Token</a>
1044- </dt>
1045- <dd tal:content="item/title">Title</dd>
1046- </tal:x>
1047- </dl>
1048- <tal:has-search condition="request/search|nothing">
1049- <tal:no-items condition="not: batchnav/currentBatch">
1050- No results found for keyword '<span tal:replace="request/search" />'.
1051- </tal:no-items>
1052- </tal:has-search>
1053- <tal:extra-bottom condition="view/extra_bottom|nothing"
1054- replace="structure view/extra_bottom" />
1055- </div>
1056- </body>
1057-</html>
1058
1059=== modified file 'lib/canonical/widgets/templates/vocabulary-picker.js'
1060--- lib/canonical/widgets/templates/vocabulary-picker.js 2009-04-23 03:32:14 +0000
1061+++ lib/canonical/widgets/templates/vocabulary-picker.js 2009-07-29 21:22:38 +0000
1062@@ -3,17 +3,34 @@
1063 return;
1064 }
1065
1066- var show_widget_node = Y.get('#%(show_widget_id)s');
1067+ // Args from python.
1068+ var args = %s;
1069+
1070+ var show_widget_node = Y.get('#' + args.show_widget_id);
1071 var save = function (result) {
1072- Y.DOM.byId('%(input_id)s').value = result.value;
1073+ Y.DOM.byId(args.input_id).value = result.value;
1074 };
1075 var config = {
1076- header: '%(header)s',
1077- step_title: '%(step_title)s'
1078+ header: args.header,
1079+ step_title: args.step_title,
1080+ extra_no_results_message: args.extra_no_results_message
1081 };
1082- var picker = Y.lp.picker.create('%(vocabulary)s', save, config);
1083+ var picker = Y.lp.picker.create(args.vocabulary, save, config);
1084+ if (config.extra_no_results_message !== null) {
1085+ picker.before('resultsChange', function (e) {
1086+ var new_results = e.details[0].newVal;
1087+ if (new_results.length === 0) {
1088+ picker.set('footer_slot',
1089+ Y.Node.create(config.extra_no_results_message));
1090+ }
1091+ else {
1092+ picker.set('footer_slot', null);
1093+ }
1094+ });
1095+ }
1096 show_widget_node.set('innerHTML', 'Choose&hellip;');
1097 show_widget_node.addClass('js-action');
1098+ show_widget_node.get('parentNode').removeClass('unseen');
1099 show_widget_node.on('click', function (e) {
1100 picker.show();
1101 e.preventDefault();
1102
1103=== modified file 'lib/lp/bugs/browser/bugalsoaffects.py'
1104--- lib/lp/bugs/browser/bugalsoaffects.py 2009-07-17 00:26:05 +0000
1105+++ lib/lp/bugs/browser/bugalsoaffects.py 2009-07-30 00:42:18 +0000
1106@@ -48,7 +48,8 @@
1107 from canonical.widgets.bugtask import (
1108 BugTaskAlsoAffectsSourcePackageNameWidget)
1109 from canonical.widgets.itemswidgets import LaunchpadRadioWidget
1110-from canonical.widgets import SearchForUpstreamPopupWidget, StrippedTextWidget
1111+from canonical.widgets.textwidgets import StrippedTextWidget
1112+from canonical.widgets.popup import SearchForUpstreamPopupWidget
1113
1114
1115 class BugAlsoAffectsProductMetaView(MultiStepView):
1116@@ -166,15 +167,18 @@
1117 # Tell the user to search for it using the popup widget as it'll allow
1118 # the user to register a new product if the one he is looking for is
1119 # not yet registered.
1120- search_url = self.widgets['product'].popupHref()
1121+ widget_link_id = self.widgets['product'].show_widget_id
1122 self.setFieldError(
1123 'product',
1124- structured(
1125- 'There is no project in Launchpad named "%s". Please '
1126- '<a href="%s">search for it</a> as it may be registered with '
1127- 'a different name.',
1128- entered_product,
1129- search_url))
1130+ structured("""
1131+ There is no project in Launchpad named "%s". Please
1132+ <a href="/projects"
1133+ onclick="YUI().use('event').Event.simulate(
1134+ document.getElementById('%s'), 'click');
1135+ return false;"
1136+ >search for it</a> as it may be
1137+ registered with a different name.""",
1138+ entered_product, widget_link_id))
1139
1140 def main_action(self, data):
1141 """Perform the 'Continue' action."""
1142
1143=== modified file 'lib/lp/bugs/interfaces/bugsupervisor.py'
1144--- lib/lp/bugs/interfaces/bugsupervisor.py 2009-06-25 00:40:31 +0000
1145+++ lib/lp/bugs/interfaces/bugsupervisor.py 2009-07-29 21:22:38 +0000
1146@@ -14,13 +14,14 @@
1147 from zope.schema import Choice
1148
1149 from canonical.launchpad import _
1150+from canonical.launchpad.fields import PublicPersonChoice
1151 from canonical.launchpad.interfaces.structuralsubscription import (
1152 IStructuralSubscriptionTarget)
1153
1154
1155 class IHasBugSupervisor(IStructuralSubscriptionTarget):
1156
1157- bug_supervisor = Choice(
1158+ bug_supervisor = PublicPersonChoice(
1159 title=_("Bug Supervisor"),
1160 description=_(
1161 "The person or team responsible for bug management."),
1162
1163=== modified file 'lib/lp/bugs/stories/bug-also-affects/20-bug-requestupstreamfix.txt'
1164--- lib/lp/bugs/stories/bug-also-affects/20-bug-requestupstreamfix.txt 2009-06-12 16:36:02 +0000
1165+++ lib/lp/bugs/stories/bug-also-affects/20-bug-requestupstreamfix.txt 2009-07-29 21:22:38 +0000
1166@@ -116,19 +116,7 @@
1167
1168 >>> search_link = user_browser.getLink('search for it')
1169 >>> search_link.url
1170- "javascript:popup_window('@@popup-search-upstream?vocabulary=Product&field=field.product&search='+escape(document.getElementById('field.product').value),'field.product','300','420')"
1171-
1172-Since the link is a javascript popup, we can't follow the link directly,
1173-so let's extract the real URL and open it manually.
1174-
1175- >>> import re
1176- >>> search_url = re.search(
1177- ... "popup_window\('([^']*)'", search_link.url).group(1)
1178- >>> user_browser.open(
1179- ... 'http://launchpad.dev/debian/+source/mozilla-firefox/+bug/3/'
1180- ... + search_url)
1181- >>> user_browser.title
1182- 'Select a project'
1183+ 'http://bugs.launchpad.dev/projects'
1184
1185 Since we don't restrict the input, the user can write anything, so we
1186 need to make sure that everything is quoted before displaying the input.
1187
1188=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-also-affects-new-upstream.txt'
1189--- lib/lp/bugs/stories/bug-also-affects/xx-also-affects-new-upstream.txt 2009-06-12 16:36:02 +0000
1190+++ lib/lp/bugs/stories/bug-also-affects/xx-also-affects-new-upstream.txt 2009-07-29 21:22:38 +0000
1191@@ -1,37 +1,14 @@
1192 = Registering an upstream affected by a given bug =
1193
1194-Sometimes users want to indicate that a bug also affects another upstream but
1195-then they realize that upstream is not yet registered in Launchpad. In order
1196-to make their life easier, we allow them to register a new upstream and
1197-indicate that it's affected by a given bug, all at once.
1198-
1199-The page where this can be done is linked to from the popup widget included in
1200-the +choose-affected-product page. Since the link is a javascript popup, we
1201-can't follow it directly, so let's extract the real URL and open it manually.
1202-
1203+The test browser does not support javascript
1204 >>> user_browser.open(
1205 ... 'http://launchpad.dev/firefox/+bug/1/+choose-affected-product')
1206- >>> choose_link = user_browser.getLink('Choose')
1207- >>> choose_link.url
1208- "javascript:popup_window('@@popup-search-upstream?vocabulary=Product&field=field.product&search='+escape(document.getElementById('field.product').value),'field.product','300','420')"
1209- >>> import re
1210- >>> choose_url = re.search(
1211- ... "popup_window\('([^']*)'", choose_link.url).group(1)
1212+ >>> find_link = user_browser.getLink('Find')
1213+ >>> find_link.url
1214+ 'http://launchpad.dev/people/'
1215+
1216 >>> user_browser.open(
1217- ... 'http://launchpad.dev/firefox/+bug/1/' + choose_url)
1218- >>> user_browser.title
1219- 'Select a project'
1220-
1221-Whenever a search is done in this page (which is rendered to the user in a
1222-popup window), a link is included at the bottom of the page inviting the user
1223-to register a new project in case he didn't find the one he was looking for.
1224-
1225- >>> user_browser.getControl(name='search').value = 'mozilla'
1226- >>> user_browser.getControl('Search').click()
1227-
1228- >>> user_browser.getLink('Register it').click()
1229- >>> user_browser.url
1230- 'http://bugs.launchpad.dev/firefox/+bug/1/+affects-new-product'
1231+ ... 'http://bugs.launchpad.dev/firefox/+bug/1/+affects-new-product')
1232 >>> user_browser.getControl('Bug URL').value = (
1233 ... 'http://bugs.foo.org/bugs/show_bug.cgi?id=42')
1234 >>> user_browser.getControl('Project name').value = 'The Foo Project'
1235
1236=== added file 'lib/lp/bugs/windmill/tests/test_bugs/test_bug_also_affects_new_upstream.py'
1237--- lib/lp/bugs/windmill/tests/test_bugs/test_bug_also_affects_new_upstream.py 1970-01-01 00:00:00 +0000
1238+++ lib/lp/bugs/windmill/tests/test_bugs/test_bug_also_affects_new_upstream.py 2009-07-30 19:03:57 +0000
1239@@ -0,0 +1,47 @@
1240+# Copyright 2009 Canonical Ltd. This software is licensed under the
1241+# GNU Affero General Public License version 3 (see the file LICENSE).
1242+
1243+from canonical.launchpad.windmill.testing.widgets import (
1244+ FormPickerWidgetTest)
1245+from canonical.launchpad.windmill.testing import lpuser
1246+from canonical.launchpad.windmill.testing.widgets import search_picker_widget
1247+from canonical.launchpad.windmill.testing.constants import (
1248+ PAGE_LOAD, FOR_ELEMENT, SLEEP)
1249+
1250+from windmill.authoring import WindmillTestClient
1251+
1252+CHOOSE_AFFECTED_URL = ('http://bugs.launchpad.dev:8085/tomcat/+bug/3/'
1253+ '+choose-affected-product')
1254+
1255+test_bug_also_affects_picker = FormPickerWidgetTest(
1256+ name='test_bug_also_affects',
1257+ url=CHOOSE_AFFECTED_URL,
1258+ short_field_name='product',
1259+ search_text='firefox',
1260+ result_index=1,
1261+ new_value='firefox')
1262+
1263+def test_bug_also_affects_register_link():
1264+ """Test that picker shows "Register it" link.
1265+
1266+ Sometimes users want to indicate that a bug also affects another upstream
1267+ but then they realize that upstream is not yet registered in Launchpad. In
1268+ order to make their life easier, we allow them to register a new upstream
1269+ and indicate that it's affected by a given bug, all at once.
1270+ """
1271+ choose_link_id = 'show-widget-field-product'
1272+ client = WindmillTestClient('test_bug_also_affects_register_link')
1273+
1274+ lpuser.SAMPLE_PERSON.ensure_login(client)
1275+
1276+ # Open a bug page and wait for it to finish loading.
1277+ client.open(url=CHOOSE_AFFECTED_URL)
1278+ client.waits.forPageLoad(timeout=PAGE_LOAD)
1279+ client.click(id=choose_link_id)
1280+ search_picker_widget(client, 'nonexistant')
1281+ client.asserts.assertProperty(
1282+ xpath=(u"//table[contains(@class, 'yui-picker') "
1283+ "and not(contains(@class, 'yui-picker-hidden'))]"
1284+ "//div[contains(@class, 'yui-picker-footer-slot')]"
1285+ "//a"),
1286+ validator=u'href|/tomcat/+bug/3/+affects-new-product')
1287
1288=== modified file 'lib/lp/registry/configure.zcml'
1289--- lib/lp/registry/configure.zcml 2009-07-23 02:06:55 +0000
1290+++ lib/lp/registry/configure.zcml 2009-07-29 15:51:00 +0000
1291@@ -546,10 +546,6 @@
1292 <allow
1293 interface="lp.registry.interfaces.sourcepackagename.ISourcePackageNameSet"/>
1294 </class>
1295- <utility
1296- name="SourcePackageName"
1297- component="lp.registry.model.sourcepackagename.SourcePackageNameVocabulary"
1298- provides="zope.schema.interfaces.IVocabularyFactory"/>
1299 <facet
1300 facet="overview">
1301
1302
1303=== modified file 'lib/lp/registry/model/sourcepackagename.py'
1304--- lib/lp/registry/model/sourcepackagename.py 2009-06-25 04:06:00 +0000
1305+++ lib/lp/registry/model/sourcepackagename.py 2009-07-29 16:22:39 +0000
1306@@ -7,25 +7,19 @@
1307 __all__ = [
1308 'SourcePackageName',
1309 'SourcePackageNameSet',
1310- 'SourcePackageNameVocabulary',
1311 'getSourcePackageDescriptions'
1312 ]
1313
1314 from zope.interface import implements
1315-from zope.schema.vocabulary import SimpleTerm
1316
1317 from sqlobject import SQLObjectNotFound
1318 from sqlobject import StringCol, SQLMultipleJoin
1319
1320 from canonical.database.sqlbase import SQLBase, quote_like, cursor, sqlvalues
1321
1322-from canonical.launchpad.webapp.vocabulary import (
1323- NamedSQLObjectHugeVocabulary)
1324 from canonical.launchpad.webapp.interfaces import NotFoundError
1325 from lp.registry.interfaces.sourcepackagename import (
1326 ISourcePackageName, ISourcePackageNameSet, NoSuchSourcePackageName)
1327-from canonical.launchpad.webapp.vocabulary import (
1328- BatchedCountableIterator)
1329
1330
1331 class SourcePackageName(SQLBase):
1332@@ -35,9 +29,10 @@
1333 name = StringCol(dbName='name', notNull=True, unique=True,
1334 alternateID=True)
1335
1336- potemplates = SQLMultipleJoin('POTemplate', joinColumn='sourcepackagename')
1337+ potemplates = SQLMultipleJoin(
1338+ 'POTemplate', joinColumn='sourcepackagename')
1339 packagings = SQLMultipleJoin(
1340- 'Packaging', joinColumn='sourcepackagename', orderBy='Packaging.id')
1341+ 'Packaging', joinColumn='sourcepackagename', orderBy='Packaging.id')
1342
1343 def __unicode__(self):
1344 return self.name
1345@@ -90,33 +85,8 @@
1346 return self.new(name)
1347
1348
1349-class SourcePackageNameIterator(BatchedCountableIterator):
1350- """A custom iterator for SourcePackageNameVocabulary.
1351-
1352- Used to iterate over vocabulary items and provide full
1353- descriptions.
1354-
1355- Note that the reason we use special iterators is to ensure that we
1356- only do the search for descriptions across source package names that
1357- we actually are attempting to list, taking advantage of the
1358- resultset slicing that BatchNavigator does.
1359- """
1360- def getTermsWithDescriptions(self, results):
1361- descriptions = getSourcePackageDescriptions(results)
1362- return [SimpleTerm(obj, obj.name,
1363- descriptions.get(obj.name, "Not yet built"))
1364- for obj in results]
1365-
1366-
1367-class SourcePackageNameVocabulary(NamedSQLObjectHugeVocabulary):
1368- """A vocabulary that lists source package names."""
1369- displayname = 'Select a Source Package'
1370- _table = SourcePackageName
1371- _orderBy = 'name'
1372- iterator = SourcePackageNameIterator
1373-
1374-
1375-def getSourcePackageDescriptions(results, use_names=False, max_title_length=50):
1376+def getSourcePackageDescriptions(
1377+ results, use_names=False, max_title_length=50):
1378 """Return a dictionary with descriptions keyed on source package names.
1379
1380 Takes an ISelectResults of a *PackageName query. The use_names
1381@@ -136,10 +106,10 @@
1382 # sourcepackagename_id and binarypackagename_id depending on
1383 # whether the row represented one or both of those cases.
1384 if use_names:
1385- clause = ("SourcePackageName.name in %s" %
1386+ clause = ("SourcePackageName.name in %s" %
1387 sqlvalues([pn.name for pn in results]))
1388 else:
1389- clause = ("SourcePackageName.id in %s" %
1390+ clause = ("SourcePackageName.id in %s" %
1391 sqlvalues([spn.id for spn in results]))
1392
1393 cur = cursor()
1394
1395=== modified file 'lib/lp/registry/vocabularies.py'
1396--- lib/lp/registry/vocabularies.py 2009-07-27 14:33:51 +0000
1397+++ lib/lp/registry/vocabularies.py 2009-07-29 03:29:24 +0000
1398@@ -43,6 +43,7 @@
1399 'ProductSeriesVocabulary',
1400 'ProductVocabulary',
1401 'ProjectVocabulary',
1402+ 'SourcePackageNameVocabulary',
1403 'UserTeamsParticipationVocabulary',
1404 'UserTeamsParticipationPlusSelfVocabulary',
1405 'ValidPersonOrTeamVocabulary',
1406@@ -90,8 +91,9 @@
1407 ILaunchBag, IStoreSelector, MAIN_STORE, DEFAULT_FLAVOR)
1408 from canonical.launchpad.webapp.tales import DateTimeFormatterAPI
1409 from canonical.launchpad.webapp.vocabulary import (
1410- CountableIterator, IHugeVocabulary, NamedSQLObjectHugeVocabulary,
1411- NamedSQLObjectVocabulary, SQLObjectVocabularyBase)
1412+ BatchedCountableIterator, CountableIterator, IHugeVocabulary,
1413+ NamedSQLObjectHugeVocabulary, NamedSQLObjectVocabulary,
1414+ SQLObjectVocabularyBase)
1415
1416 from lp.registry.interfaces.distribution import IDistribution
1417 from lp.registry.interfaces.distributionsourcepackage import (
1418@@ -120,6 +122,7 @@
1419 from lp.registry.model.productrelease import ProductRelease
1420 from lp.registry.model.productseries import ProductSeries
1421 from lp.registry.model.project import Project
1422+from lp.registry.model.sourcepackagename import SourcePackageName
1423
1424
1425 class BasePersonVocabulary:
1426@@ -1459,3 +1462,26 @@
1427 AND PillarName.name = %s""" % sqlvalues(obj.name)
1428 return PillarName.selectOne(
1429 query, clauseTables=['FeaturedProject']) is not None
1430+
1431+
1432+class SourcePackageNameIterator(BatchedCountableIterator):
1433+ """A custom iterator for SourcePackageNameVocabulary.
1434+
1435+ Used to iterate over vocabulary items and provide full
1436+ descriptions.
1437+
1438+ Note that the reason we use special iterators is to ensure that we
1439+ only do the search for descriptions across source package names that
1440+ we actually are attempting to list, taking advantage of the
1441+ resultset slicing that BatchNavigator does.
1442+ """
1443+ def getTermsWithDescriptions(self, results):
1444+ return [SimpleTerm(obj, obj.name, obj.name) for obj in results]
1445+
1446+
1447+class SourcePackageNameVocabulary(NamedSQLObjectHugeVocabulary):
1448+ """A vocabulary that lists source package names."""
1449+ displayname = 'Select a source package'
1450+ _table = SourcePackageName
1451+ _orderBy = 'name'
1452+ iterator = SourcePackageNameIterator
1453
1454=== modified file 'lib/lp/registry/vocabularies.zcml'
1455--- lib/lp/registry/vocabularies.zcml 2009-07-17 00:26:05 +0000
1456+++ lib/lp/registry/vocabularies.zcml 2009-07-29 03:29:24 +0000
1457@@ -189,4 +189,9 @@
1458 provides="zope.schema.interfaces.IVocabularyFactory"
1459 />
1460
1461+ <utility
1462+ name="SourcePackageName"
1463+ component="lp.registry.vocabularies.SourcePackageNameVocabulary"
1464+ provides="zope.schema.interfaces.IVocabularyFactory"
1465+ />
1466 </configure>
1467
1468=== removed file 'lib/lp/soyuz/stories/soyuz/xx-search-for-binary-and-source-packages.txt'
1469--- lib/lp/soyuz/stories/soyuz/xx-search-for-binary-and-source-packages.txt 2007-04-14 10:26:11 +0000
1470+++ lib/lp/soyuz/stories/soyuz/xx-search-for-binary-and-source-packages.txt 1970-01-01 00:00:00 +0000
1471@@ -1,9 +0,0 @@
1472-Get the BinaryAndSourcePackageName vocabulary popup, searching for 'firefox'.
1473-
1474- >>> print http(r"""
1475- ... GET /ubuntu/+filebug-advanced/@@popup-window?search=firefox&vocabulary=BinaryAndSourcePackageName&field=field.packagename HTTP/1.1
1476- ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
1477- ... """)
1478- HTTP/1.1 200 Ok
1479- ...mozilla-firefox...
1480-