Merge lp:~edwin-grubbs/launchpad/bug-405300-remove-simplepopupwidget into lp:launchpad
- bug-405300-remove-simplepopupwidget
- Merge into devel
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 | ||||
Related bugs: |
|
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 |
Commit message
Description of the change
Edwin Grubbs (edwin-grubbs) wrote : | # |
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:/
Any technical why that doesn't happen?
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:/
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,
ObjectImageDisp
which object type maps to which sprite. Until that is all taken care
of, I think the default icon makes it look nicer.
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,
> ObjectImageDisp
> 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?
Martin Albisetti (beuno) : | # |
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/
> --- lib/canonical/
> @@ -87,6 +94,16 @@
> extra.description = branch.bzr_identity
> return extra
>
> +@implementer(
> +@adapter(
> +def sourcepackagena
> + """Adapts IBranch to IPickerEntry."""
> + extra = default_
> + descriptions = getSourcePackag
> + extra.description = descriptions.get(
> + sourcepackagena
> + return extra
> +
>
> class HugeVocabularyJ
> """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 = IWebServiceClie
> - entry['api_uri'] = canonical_url(
> - term.value, request=
> path_only_
> + try:
> + entry['api_uri'] = canonical_url(
> + term.value, request=
> + path_only_
> + except NoCanonicalUrl:
> + entry['api_uri'] = 'Could not find canonical url.'
> picker_entry = IPickerEntry(
> if picker_
> if len(picker_
>
As discussed in IRC, please add a comment explaining why it's okay to let this
exception continue.
Preview Diff
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=""><script>alert(Y0u |
338 | - @r3 0wn3d!!!);</script>" 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…</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…</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…</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…</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">←</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">→</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…'); |
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 | - |
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: meVocabulary. The
* 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 SourcePackageNa
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. canonical/ launchpad/ zcml/launchpad. zcml
lib/
Deleted: canonical/ launchpad/ doc/popup- view.txt canonical/ launchpad/ doc/popup- widget. txt canonical/ launchpad/ pagetests/ standalone/ xx-show- people- with-password- only.txt canonical/ widgets/ templates/ popup-window. pt lp/soyuz/ stories/ soyuz/xx- search- for-binary- and-source- packages. txt
lib/
lib/
lib/
lib/
lib/
Changes to the SourcePackageNa meVocabulary as described above. canonical/ launchpad/ doc/vocabularie s.txt
lib/
Add adapter so that the PersonPickerWidget is automatically used for canonical/ launchpad/ webapp/ configure. zcml
person form attributes. This is just necessary to add the
"Create Team" link after the "Choose" link.
lib/
Windmill tests: canonical/ launchpad/ windmill/ testing/ widgets. py lp/bugs/ windmill/ tests/test_ bugs/test_ bug_also_ affects_ new_upstream. py
lib/
lib/
Remove references to the old popup widget. The windmill test should canonical/ widgets/ __init_ _.py canonical/ widgets/ bugtask. py canonical/ widgets/ project. py canonical/ launchpad/ doc/project- scope-widget. txt lp/bugs/ stories/ bug-also- affects/ 20-bug- requestupstream fix.txt lp/bugs/ stories/ bug-also- affects/ xx-also- affects- new-upstream. txt
cover the most important aspect, but not test every form that had a
weak test for the old popup widget.
lib/
lib/
lib/
lib/
lib/
lib/
This was always unnecessary. canonical/ widgets/ configure. zcml
lib/
Remove old widget and migrate necessary methods that are used by rWidget, which previously inherited from the old widget. canonical/ widgets/ popup.py
VocabularyPicke
lib/
Fixed lint error. canonical/ widgets/ templates/ form-picker. pt
lib/
Add ability to send a special error message when no results are returned. canonical/ widgets/ templates/ vocabulary- picker. js
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/
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"...