Merge lp:~sinzui/launchpad/bug-tracker-widget-0 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Edwin Grubbs
Approved revision: no longer in the source branch.
Merged at revision: 11028
Proposed branch: lp:~sinzui/launchpad/bug-tracker-widget-0
Merge into: lp:launchpad
Diff against target: 641 lines (+308/-40)
14 files modified
lib/canonical/launchpad/icing/style-3-0.css.in (+9/-0)
lib/canonical/launchpad/vocabularies/configure.zcml (+12/-0)
lib/canonical/launchpad/vocabularies/dbobjects.py (+44/-4)
lib/canonical/widgets/product.py (+52/-18)
lib/canonical/widgets/templates/product-bug-tracker.pt (+24/-0)
lib/lp/bugs/browser/bugtarget.py (+6/-1)
lib/lp/bugs/browser/tests/test_bugtarget_configure.py (+1/-1)
lib/lp/bugs/stories/bug-also-affects/xx-upstream-bugtracker-links.txt (+4/-5)
lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt (+1/-1)
lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt (+2/-3)
lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt (+1/-1)
lib/lp/bugs/tests/test_bugtracker_vocabulary.py (+123/-0)
lib/lp/registry/doc/product-widgets.txt (+28/-5)
lib/lp/registry/stories/project/xx-project-edit.txt (+1/-1)
To merge this branch: bzr merge lp:~sinzui/launchpad/bug-tracker-widget-0
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) code ui Approve
Review via email: mp+27741@code.launchpad.net

Description of the change

This is my branch to include subordinate fields in the bug tracker widget.
This change updated of the bug tracker widget to the proposed design.
http://people.canonical.com/~curtis/product-bug-tracker-widget.png

    lp:~sinzui/launchpad/bug-tracker-widget-0
    Diff size: 600
    Launchpad bug:
          https://bugs.launchpad.net/bugs/539083
    Test command: ./bin/test -vv \
          -t product-widgets -t test_bugtracker_vocabulary \
          -t test_bugtarget_configure -t xx-product-launchpad-usage \
          -t xx-project-guided-filebug -t bugtrackers-index \
          -t xx-bugtracker.txt -t xx-upstream-bug-tracker-links
    Pre-implementation: bac, edwin, deryck, gary
    Target release: 10.06

Include subordinate fields in the bug tracker widget
----------------------------------------------------

The bugtracker widget does not know that the remote project field is
subordinate to choosing a another bugtracker. The expiration widget is
subordinate to the launchpad bug tracker, but the field is not displayed under
the radio option.

The bugtracker widget could render all the fields in a uniformed way so that it
is clear when the fields are available.

Rules
-----

I considered updating the javascript to control the constraint on
remote_product, but the form does not have enough information to know
when the enable/disabled to field. We need to know the bug tracker type,
which cannot be provided by the picker widget. I focused on making the
widget look the the agreed design.

    * Add the remote_product and expiration fields to the widget's template
    * Switch the registered bug tracker widget to a picker

QA
--

    * Visit a project and choose to configure bugs.
    * Verify that the two fields are shown to be subordinate.
    * Verify that the registered bug tracker widget is a picker

Lint
----

Linting changed files:
  lib/canonical/launchpad/icing/style-3-0.css.in
  lib/canonical/launchpad/vocabularies/configure.zcml
  lib/canonical/launchpad/vocabularies/dbobjects.py
  lib/canonical/widgets/product.py
  lib/canonical/widgets/templates/product-bug-tracker.pt
  lib/lp/bugs/browser/bugtarget.py
  lib/lp/bugs/browser/tests/test_bugtarget_configure.py
  lib/lp/bugs/stories/bug-also-affects/xx-upstream-bugtracker-links.txt
  lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt
  lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt
  lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt
  lib/lp/bugs/tests/test_bugtracker_vocabulary.py
  lib/lp/registry/doc/product-widgets.txt

Test
----

    * lib/lp/bugs/browser/tests/test_bugtarget_configure.py
    * lib/lp/bugs/stories/bug-also-affects/xx-upstream-bugtracker-links.txt
    * lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt
    * lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt
    * lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt
      * Updated these tests to use the text field; it is not a select widget
        and the tests were not good since users do not see bugtrackers ids.

    * lib/lp/bugs/tests/test_bugtracker_vocabulary.py
      * Added a test to verify the behaviours to BugTracker vocabs.
    * lib/lp/registry/doc/product-widgets.txt
      * Updated the test to recognise the subordinate fields and the
        change to the registered bug tracker widget.

Implementation
--------------

    * lib/canonical/launchpad/icing/style-3-0.css.in
      * Added a class to show subordinate controls as indented.
    * lib/canonical/launchpad/vocabularies/configure.zcml
      * Registered the WebBugTrackerVocabulary since the picker widget
        gets it via the VocabularyRegistry
    * lib/canonical/launchpad/vocabularies/dbobjects.py
      * Updated BugTrackerVocabulary to be an IHugeVocabulary. The data
        has grown too large for a normal vocabulary. The picker requires
        a IHugeVocabulary.search(query) method.
    * lib/canonical/widgets/product.py
      * Updated ProductBugTrackerWidget to use VocabularyPickerWidget for
        the registered bug tracker field. Redefined the renderValue()
        to work with a dict of item controls and create the two subordinate
        controls. The method then calls the new template to do the
        layout.
      * Extracted the ghost rules from GhostWidget so to create GhostCheckBox
        widget.
    * lib/canonical/widgets/templates/product-bug-tracker.pt
      * Added a template to render the ProductBugTrackerWidget with
        subordinate controls.
    * lib/lp/bugs/browser/bugtarget.py
      * Updated ProductBugConfigurationView to use ghost widgets for the
        remote_product and enable_bug_expiration fields so that the form
        continues to manage state and validation, but the ProductBugTracker
        Widget does the rendering.

To post a comment you must log in.
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

Hi Curtis,

This looks good. I think the radio buttons could be spaced out from each other a little more, and "In a registered bug tracker" might sound better as "In an external bug tracker".

Just one comment on the code below.

-Edwin

>=== modified file 'lib/canonical/launchpad/vocabularies/dbobjects.py'
>--- lib/canonical/launchpad/vocabularies/dbobjects.py 2010-05-16 23:56:51 +0000
>+++ lib/canonical/launchpad/vocabularies/dbobjects.py 2010-06-16 21:35:08 +0000
>@@ -226,15 +229,52 @@
>
>
> class BugTrackerVocabulary(SQLObjectVocabularyBase):
>-
>+ """All web and email based external bug trackers."""
>+ displayname = 'Select a bug tracker'
>+ implements(IHugeVocabulary)
> _table = BugTracker
>+ _filter = 1 == 1

Why not just use?
    _filter = True

review: Approve (code ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/icing/style-3-0.css.in'
2--- lib/canonical/launchpad/icing/style-3-0.css.in 2010-06-11 18:39:35 +0000
3+++ lib/canonical/launchpad/icing/style-3-0.css.in 2010-06-17 19:17:25 +0000
4@@ -716,6 +716,12 @@
5 form table label {
6 font-weight: bold;
7 }
8+.compound {
9+ margin-bottom: .5em;
10+ }
11+.field.subordinate label {
12+ font-weight: normal;
13+ }
14 fieldset {
15 border-width: 2px 0 0;
16 margin: 1em 0;
17@@ -747,6 +753,9 @@
18 .fieldRequired, .fieldOptional {
19 color: #999;
20 }
21+.field.subordinate {
22+ margin-left: 2.6em;
23+ }
24 .formHelp {
25 margin: 0.2em 0 0.5em 0.2em;
26 color: #777;
27
28=== modified file 'lib/canonical/launchpad/vocabularies/configure.zcml'
29--- lib/canonical/launchpad/vocabularies/configure.zcml 2010-01-11 18:59:53 +0000
30+++ lib/canonical/launchpad/vocabularies/configure.zcml 2010-06-17 19:17:25 +0000
31@@ -95,6 +95,18 @@
32
33
34 <securedutility
35+ name="WebBugTracker"
36+ component="canonical.launchpad.vocabularies.WebBugTrackerVocabulary"
37+ provides="zope.schema.interfaces.IVocabularyFactory"
38+ >
39+ <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
40+ </securedutility>
41+
42+ <class class="canonical.launchpad.vocabularies.WebBugTrackerVocabulary">
43+ <allow interface="canonical.launchpad.webapp.vocabulary.IHugeVocabulary"/>
44+ </class>
45+
46+ <securedutility
47 name="BugWatch"
48 component="canonical.launchpad.vocabularies.BugWatchVocabulary"
49 provides="zope.schema.interfaces.IVocabularyFactory"
50
51=== modified file 'lib/canonical/launchpad/vocabularies/dbobjects.py'
52--- lib/canonical/launchpad/vocabularies/dbobjects.py 2010-05-16 23:56:51 +0000
53+++ lib/canonical/launchpad/vocabularies/dbobjects.py 2010-06-17 19:17:25 +0000
54@@ -47,13 +47,16 @@
55 import cgi
56 from operator import attrgetter
57
58-from sqlobject import AND, SQLObjectNotFound
59+from sqlobject import AND, CONTAINSSTRING, SQLObjectNotFound
60 from storm.expr import SQL
61 from zope.component import getUtility
62 from zope.interface import implements
63 from zope.schema.interfaces import IVocabulary, IVocabularyTokenized
64 from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
65
66+from storm.expr import And, Or
67+
68+from canonical.launchpad.interfaces.lpstorm import IStore
69 from lp.code.model.branch import Branch
70 from lp.bugs.model.bug import Bug
71 from lp.bugs.model.bugtracker import BugTracker
72@@ -226,15 +229,52 @@
73
74
75 class BugTrackerVocabulary(SQLObjectVocabularyBase):
76-
77+ """All web and email based external bug trackers."""
78+ displayname = 'Select a bug tracker'
79+ implements(IHugeVocabulary)
80 _table = BugTracker
81+ _filter = True
82 _orderBy = 'title'
83+ _order_by = [BugTracker.title]
84+
85+ def toTerm(self, obj):
86+ """See `IVocabulary`."""
87+ return SimpleTerm(obj, obj.name, obj.title)
88+
89+ def getTermByToken(self, token):
90+ """See `IVocabularyTokenized`."""
91+ result = IStore(self._table).find(
92+ self._table,
93+ self._filter,
94+ BugTracker.name == token).one()
95+ if result is None:
96+ raise LookupError(token)
97+ return self.toTerm(result)
98+
99+ def search(self, query):
100+ """Search for web bug trackers."""
101+ query = query.lower()
102+ results = IStore(self._table).find(
103+ self._table, And(
104+ self._filter,
105+ BugTracker.active == True,
106+ Or(
107+ CONTAINSSTRING(BugTracker.name, query),
108+ CONTAINSSTRING(BugTracker.title, query),
109+ CONTAINSSTRING(BugTracker.summary, query),
110+ CONTAINSSTRING(BugTracker.baseurl, query))))
111+ results = results.order_by(self._order_by)
112+ return results
113+
114+ def searchForTerms(self, query=None):
115+ """See `IHugeVocabulary`."""
116+ results = self.search(query)
117+ return CountableIterator(results.count(), results, self.toTerm)
118
119
120 class WebBugTrackerVocabulary(BugTrackerVocabulary):
121 """All web-based bug tracker types."""
122-
123- _filter = BugTracker.q.bugtrackertype != BugTrackerType.EMAILADDRESS
124+ _filter = BugTracker.bugtrackertype != BugTrackerType.EMAILADDRESS
125
126
127 class LanguageVocabulary(SQLObjectVocabularyBase):
128
129=== modified file 'lib/canonical/widgets/product.py'
130--- lib/canonical/widgets/product.py 2010-06-15 03:08:22 +0000
131+++ lib/canonical/widgets/product.py 2010-06-17 19:17:25 +0000
132@@ -5,6 +5,7 @@
133
134 __metaclass__ = type
135 __all__ = [
136+ 'GhostCheckBoxWidget',
137 'GhostWidget',
138 'LicenseWidget',
139 'ProductBugTrackerWidget',
140@@ -15,6 +16,7 @@
141 import math
142
143 from zope.app.form import CustomWidgetFactory
144+from zope.app.form.browser.boolwidgets import CheckBoxWidget
145 from zope.app.form.browser.textwidgets import TextWidget
146 from zope.app.form.browser.widget import renderElement
147 from zope.app.form.interfaces import IInputWidget
148@@ -24,25 +26,28 @@
149
150 from z3c.ptcompat import ViewPageTemplateFile
151
152+from lazr.restful.interface import copy_field
153+
154 from canonical.launchpad.browser.widgets import DescriptionWidget
155 from canonical.launchpad.fields import StrippedTextLine
156 from canonical.launchpad.interfaces import (
157 BugTrackerType, IBugTracker, IBugTrackerSet, ILaunchBag)
158 from canonical.launchpad.validators import LaunchpadValidationError
159 from canonical.launchpad.validators.email import email_validator
160-from canonical.launchpad.vocabularies.dbobjects import (
161- WebBugTrackerVocabulary)
162 from canonical.launchpad.webapp import canonical_url
163 from canonical.widgets.itemswidgets import (
164- CheckBoxMatrixWidget, LaunchpadDropdownWidget, LaunchpadRadioWidget)
165+ CheckBoxMatrixWidget, LaunchpadRadioWidget)
166+from canonical.widgets.popup import VocabularyPickerWidget
167 from canonical.widgets.textwidgets import (
168 LowerCaseTextWidget, StrippedTextWidget)
169+from lp.registry.interfaces.product import IProduct
170
171
172 class ProductBugTrackerWidget(LaunchpadRadioWidget):
173 """Widget for selecting a product bug tracker."""
174
175 _joinButtonToMessageTemplate = u'%s&nbsp;%s'
176+ template = ViewPageTemplateFile('templates/product-bug-tracker.pt')
177
178 def __init__(self, field, vocabulary, request):
179 # pylint: disable-msg=W0233
180@@ -50,19 +55,15 @@
181
182 # Bug tracker widget.
183 self.bugtracker = Choice(
184- vocabulary=WebBugTrackerVocabulary(),
185+ vocabulary="WebBugTracker",
186 __name__='bugtracker')
187- self.bugtracker_widget = CustomWidgetFactory(LaunchpadDropdownWidget)
188+ self.bugtracker_widget = CustomWidgetFactory(VocabularyPickerWidget)
189 setUpWidget(
190 self, 'bugtracker', self.bugtracker, IInputWidget,
191 prefix=self.name, value=field.context.bugtracker,
192 context=field.context)
193- if self.bugtracker_widget.extra is None:
194- self.bugtracker_widget.extra = ''
195- ## Select the corresponding radio option automatically if
196- ## the user selects a bug tracker.
197- self.bugtracker_widget.extra += (
198- ' onchange="selectWidget(\'%s.2\', event);"' % self.name)
199+ self.bugtracker_widget.onKeyPress = (
200+ "selectWidget('%s.2', event);" % self.name)
201
202 # Upstream email address field and widget.
203 ## This is to make email address bug trackers appear
204@@ -198,10 +199,12 @@
205 value="external-email", name=self.name, cssClass=self.cssClass)
206
207 # All the choices arguments in order.
208- all_arguments = [malone_item_arguments,
209- external_bugtracker_arguments,
210- external_bugtracker_email_arguments,
211- project_bugtracker_arguments]
212+ all_arguments = {
213+ 'launchpad': malone_item_arguments,
214+ 'external_bugtracker': external_bugtracker_arguments,
215+ 'external_email': external_bugtracker_email_arguments,
216+ 'unknown': project_bugtracker_arguments,
217+ }
218
219 # Figure out the selected choice.
220 if value == field.malone_marker:
221@@ -219,12 +222,35 @@
222 selected = project_bugtracker_arguments
223
224 # Render.
225- for arguments in all_arguments:
226+ for name, arguments in all_arguments.items():
227 if arguments is selected:
228 render = self.renderSelectedItem
229 else:
230 render = self.renderItem
231- yield render(**arguments)
232+ yield (name, render(**arguments))
233+
234+ def renderValue(self, value):
235+ # Render the items with subordinate fields and support markup.
236+ self.bug_trackers = dict(self.renderItems(value))
237+ self.product = self.context.context
238+ # The view must also use GhostWidget for the 'remote_product' field.
239+ self.remote_product = copy_field(IProduct['remote_product'])
240+ self.remote_product_widget = CustomWidgetFactory(TextWidget)
241+ setUpWidget(
242+ self, 'remote_product', self.remote_product, IInputWidget,
243+ prefix='field', value=self.product.remote_product,
244+ context=self.product)
245+ # The view must also use GhostWidget for the 'enable_bug_expiration'
246+ # field.
247+ self.enable_bug_expiration = copy_field(
248+ IProduct['enable_bug_expiration'])
249+ self.enable_bug_expiration_widget = CustomWidgetFactory(
250+ CheckBoxWidget)
251+ setUpWidget(
252+ self, 'enable_bug_expiration', self.enable_bug_expiration,
253+ IInputWidget, prefix='field',
254+ value=self.product.enable_bug_expiration, context=self.product)
255+ return self.template()
256
257
258 class LicenseWidget(CheckBoxMatrixWidget):
259@@ -406,7 +432,7 @@
260 return 'text'
261
262
263-class GhostWidget(TextWidget):
264+class GhostMixin:
265 """A simple widget that has no HTML."""
266 visible = False
267 # This suppresses the stuff above the widget.
268@@ -420,3 +446,11 @@
269 return ''
270
271 hidden = __call__
272+
273+
274+class GhostWidget(GhostMixin, TextWidget):
275+ """Suppress the rendering of Text input fields."""
276+
277+
278+class GhostCheckBoxWidget(GhostMixin, CheckBoxWidget):
279+ """Suppress the rendering of Bool input fields."""
280
281=== added file 'lib/canonical/widgets/templates/product-bug-tracker.pt'
282--- lib/canonical/widgets/templates/product-bug-tracker.pt 1970-01-01 00:00:00 +0000
283+++ lib/canonical/widgets/templates/product-bug-tracker.pt 2010-06-17 19:17:25 +0000
284@@ -0,0 +1,24 @@
285+<tal:root
286+ xmlns:tal="http://xml.zope.org/namespaces/tal"
287+ xmlns:metal="http://xml.zope.org/namespaces/metal"
288+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
289+ omit-tag="">
290+ <div class="compound">
291+ <div tal:content="structure view/bug_trackers/launchpad" />
292+ <div class="field subordinate">
293+ <input tal:replace="structure view/enable_bug_expiration_widget" />
294+ <label
295+ tal:attributes="for view/enable_bug_expiration_widget/name"
296+ tal:content="view/enable_bug_expiration/title" />
297+ </div>
298+ </div>
299+ <div class="compound">
300+ <div tal:content="structure view/bug_trackers/external_bugtracker" />
301+ <div class="field subordinate">
302+ Project ID in bug tracker:
303+ <input tal:replace="structure view/remote_product_widget" />
304+ </div>
305+ </div>
306+ <div class="compound" tal:content="structure view/bug_trackers/external_email" />
307+ <div class="compound" tal:content="structure view/bug_trackers/unknown" />
308+</tal:root>
309
310=== modified file 'lib/lp/bugs/browser/bugtarget.py'
311--- lib/lp/bugs/browser/bugtarget.py 2010-06-15 11:35:12 +0000
312+++ lib/lp/bugs/browser/bugtarget.py 2010-06-17 19:17:25 +0000
313@@ -98,7 +98,8 @@
314 from canonical.launchpad.webapp.publisher import HTTP_MOVED_PERMANENTLY
315 from canonical.widgets.bug import BugTagsWidget, LargeBugTagsWidget
316 from canonical.widgets.bugtask import NewLineToSpacesWidget
317-from canonical.widgets.product import ProductBugTrackerWidget
318+from canonical.widgets.product import (
319+ ProductBugTrackerWidget, GhostCheckBoxWidget, GhostWidget)
320 from lp.registry.vocabularies import ValidPersonOrTeamVocabulary
321
322
323@@ -145,7 +146,11 @@
324 "bug_reporting_guidelines",
325 "bug_reported_acknowledgement",
326 ]
327+ # This ProductBugTrackerWidget renders enable_bug_expiration and
328+ # remote_product as subordinate fields, so this view suppresses them.
329 custom_widget('bugtracker', ProductBugTrackerWidget)
330+ custom_widget('enable_bug_expiration', GhostCheckBoxWidget)
331+ custom_widget('remote_product', GhostWidget)
332
333 def validate(self, data):
334 """Constrain bug expiration to Launchpad Bugs tracker."""
335
336=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_configure.py'
337--- lib/lp/bugs/browser/tests/test_bugtarget_configure.py 2010-06-14 15:52:41 +0000
338+++ lib/lp/bugs/browser/tests/test_bugtarget_configure.py 2010-06-17 19:17:25 +0000
339@@ -105,7 +105,7 @@
340 form = self._makeForm()
341 form['field.enable_bug_expiration'] = 'on'
342 form['field.bugtracker'] = 'external'
343- form['field.bugtracker.bugtracker'] = '3'
344+ form['field.bugtracker.bugtracker'] = 'debbugs'
345 view = create_initialized_view(
346 self.product, name='+configure-bugtracker', form=form)
347 self.assertEqual([], view.errors)
348
349=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-upstream-bugtracker-links.txt'
350--- lib/lp/bugs/stories/bug-also-affects/xx-upstream-bugtracker-links.txt 2010-06-16 16:47:54 +0000
351+++ lib/lp/bugs/stories/bug-also-affects/xx-upstream-bugtracker-links.txt 2010-06-17 19:17:25 +0000
352@@ -33,7 +33,7 @@
353 >>> admin_browser.getControl(
354 ... name='field.bugtracker').value = ['external']
355 >>> admin_browser.getControl(
356- ... name='field.bugtracker.bugtracker').value = ['1']
357+ ... name='field.bugtracker.bugtracker').value = 'mozilla.org'
358 >>> admin_browser.getControl('Change').click()
359
360 >>> user_browser.open('http://launchpad.dev/bugs/13/')
361@@ -95,8 +95,7 @@
362 >>> admin_browser.getControl(
363 ... 'In a registered bug tracker:').selected = True
364 >>> admin_browser.getControl(
365- ... name='field.bugtracker.bugtracker').displayValue = [
366- ... 'Debian Bug tracker']
367+ ... name='field.bugtracker.bugtracker').value = 'debbugs'
368 >>> admin_browser.getControl('Change').click()
369
370 >>> user_browser.open('http://launchpad.dev/bugs/13/')
371@@ -122,11 +121,11 @@
372
373 >>> admin_browser.open(
374 ... 'http://launchpad.dev/thunderbird/+configure-bugtracker')
375- >>> admin_browser.getControl('Remote bug tracker project id').value = (
376+ >>> admin_browser.getControl(name='field.remote_product').value = (
377 ... 'Thunderbird')
378 >>> admin_browser.getControl('Change').click()
379
380 >>> admin_browser.open(
381 ... 'http://launchpad.dev/thunderbird/+configure-bugtracker')
382- >>> print admin_browser.getControl('Remote bug tracker project id').value
383+ >>> print admin_browser.getControl(name='field.remote_product').value
384 Thunderbird
385
386=== modified file 'lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt'
387--- lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt 2010-05-19 05:47:50 +0000
388+++ lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt 2010-06-17 19:17:25 +0000
389@@ -92,7 +92,7 @@
390 ... "http://launchpad.dev/%s/+configure-bugtracker" % name)
391 ... admin_browser.getControl("In a registered bug tracker").click()
392 ... bt = admin_browser.getControl(name="field.bugtracker.bugtracker")
393- ... bt.value = ["3"]
394+ ... bt.value = 'debbugs'
395 ... admin_browser.getControl("Change").click()
396 ...
397 >>> link_to_debbugs('upstart')
398
399=== modified file 'lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt'
400--- lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt 2010-04-09 19:11:36 +0000
401+++ lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt 2010-06-17 19:17:25 +0000
402@@ -668,8 +668,7 @@
403 bug tracker:
404
405 >>> admin_browser.open('http://launchpad.dev/mozilla/+edit')
406- >>> admin_browser.getControl('Bug Tracker:').displayValue = [
407- ... 'A test Bugzilla Tracker']
408+ >>> admin_browser.getControl('Bug Tracker:').value = 'testbugzilla'
409 >>> admin_browser.getControl('Change Details').click()
410
411 >>> admin_browser.open(
412@@ -677,7 +676,7 @@
413 >>> admin_browser.getControl(name='field.bugtracker'
414 ... ).displayValue = ['In a registered bug tracker:']
415 >>> admin_browser.getControl(name='field.bugtracker.bugtracker'
416- ... ).displayValue = ['A test Bugzilla Tracker']
417+ ... ).value = 'testbugzilla'
418 >>> admin_browser.getControl('Change').click()
419
420 Now the Mozilla Project and Jokosher will appear in the Related
421
422=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt'
423--- lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt 2010-04-16 15:06:55 +0000
424+++ lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt 2010-06-17 19:17:25 +0000
425@@ -220,7 +220,7 @@
426 >>> admin_browser.open('http://launchpad.dev/testy/+configure-bugtracker')
427 >>> admin_browser.getControl(name='field.bugtracker').value = ['external']
428 >>> admin_browser.getControl(
429- ... name='field.bugtracker.bugtracker').value = ['3']
430+ ... name='field.bugtracker.bugtracker').value = 'debbugs'
431 >>> admin_browser.getControl('Change').click()
432
433 And on the filebug page...
434
435=== added file 'lib/lp/bugs/tests/test_bugtracker_vocabulary.py'
436--- lib/lp/bugs/tests/test_bugtracker_vocabulary.py 1970-01-01 00:00:00 +0000
437+++ lib/lp/bugs/tests/test_bugtracker_vocabulary.py 2010-06-17 19:17:25 +0000
438@@ -0,0 +1,123 @@
439+# Copyright 2010 Canonical Ltd. This software is licensed under the
440+# GNU Affero General Public License version 3 (see the file LICENSE).
441+
442+"""Test the bug tracker vocabularies."""
443+
444+__metaclass__ = type
445+
446+from unittest import TestLoader
447+
448+from zope.schema.vocabulary import getVocabularyRegistry
449+
450+from canonical.launchpad.ftests import login, login_person
451+from canonical.testing import DatabaseFunctionalLayer
452+from lp.bugs.interfaces.bugtracker import BugTrackerType
453+from lp.testing import TestCaseWithFactory
454+
455+
456+class TestBugTrackerVocabulary(TestCaseWithFactory):
457+
458+ layer = DatabaseFunctionalLayer
459+
460+ def setUp(self):
461+ super(TestBugTrackerVocabulary, self).setUp()
462+ vocabulary_registry = getVocabularyRegistry()
463+ self.vocab = vocabulary_registry.get(None, 'BugTracker')
464+
465+ def test_toTerm(self):
466+ # Verify the data in the term.
467+ bug_tracker = self.factory.makeBugTracker()
468+ login_person(bug_tracker.owner)
469+ bug_tracker.name = 'weasel'
470+ bug_tracker.title = 'The Weasel Bug Tracker'
471+ [term] = self.vocab.searchForTerms('weasel')
472+ self.assertEqual(bug_tracker, term.value)
473+ self.assertEqual(bug_tracker.name, term.token)
474+ self.assertEqual(bug_tracker.title, term.title)
475+
476+ def test_getTermByToken_match(self):
477+ # Verify the token by lookup.
478+ bug_tracker = self.factory.makeBugTracker()
479+ login_person(bug_tracker.owner)
480+ bug_tracker.name = 'mink'
481+ term = self.vocab.getTermByToken('mink')
482+ self.assertEqual(bug_tracker, term.value)
483+
484+ def test_getTermByToken_no_match(self):
485+ # Verify that a LookupError error is raised.
486+ self.assertRaises(
487+ LookupError, self.vocab.getTermByToken, 'does not exist')
488+
489+ def searchForBugTrackers(self, query):
490+ terms = self.vocab.searchForTerms(query)
491+ return [term.value for term in terms]
492+
493+ def test_search_name(self):
494+ # Verify that queries match name text.
495+ bug_tracker = self.factory.makeBugTracker()
496+ login_person(bug_tracker.owner)
497+ bug_tracker.name = 'skunkworks'
498+ bug_trackers = self.searchForBugTrackers('skunk')
499+ self.assertEqual([bug_tracker], bug_trackers)
500+
501+ def test_search_title(self):
502+ # Verify that queries match title text.
503+ bug_tracker = self.factory.makeBugTracker()
504+ login_person(bug_tracker.owner)
505+ bug_tracker.title = 'A ferret in your pants'
506+ bug_trackers = self.searchForBugTrackers('ferret')
507+ self.assertEqual([bug_tracker], bug_trackers)
508+
509+ def test_search_summary(self):
510+ # Verify that queries match summary text.
511+ bug_tracker = self.factory.makeBugTracker()
512+ login_person(bug_tracker.owner)
513+ bug_tracker.summary = 'A badger is a member of the weasel family.'
514+ bug_trackers = self.searchForBugTrackers('badger')
515+ self.assertEqual([bug_tracker], bug_trackers)
516+
517+ def test_search_baseurl(self):
518+ # Verify that queries match baseurl text.
519+ bug_tracker = self.factory.makeBugTracker(
520+ base_url='http://bugs.otter.dom/')
521+ bug_trackers = self.searchForBugTrackers('otter')
522+ self.assertEqual([bug_tracker], bug_trackers)
523+
524+ def test_search_inactive(self):
525+ # Verify that inactive bug trackers are not returned by search,
526+ # but are in the vocabulary.
527+ bug_tracker = self.factory.makeBugTracker()
528+ login('admin@canonical.com')
529+ bug_tracker.name = 'stoat'
530+ bug_tracker.active = False
531+ bug_trackers = self.searchForBugTrackers('stoat')
532+ self.assertEqual([], bug_trackers)
533+ term = self.vocab.getTermByToken('stoat')
534+ self.assertEqual(bug_tracker, term.value)
535+
536+
537+class TestWebBugTrackerVocabulary(TestCaseWithFactory):
538+
539+ layer = DatabaseFunctionalLayer
540+
541+ def setUp(self):
542+ super(TestWebBugTrackerVocabulary, self).setUp()
543+ vocabulary_registry = getVocabularyRegistry()
544+ self.vocab = vocabulary_registry.get(None, 'WebBugTracker')
545+
546+ def test_search_no_email_type(self):
547+ # Verify that emailaddress bug trackers are not returned by search,
548+ # and are not in the vocabulary.
549+ bug_tracker = self.factory.makeBugTracker(
550+ bugtrackertype=BugTrackerType.EMAILADDRESS)
551+ login_person(bug_tracker.owner)
552+ bug_tracker.name = 'marten'
553+ terms = self.vocab.searchForTerms('marten')
554+ bug_trackers = [term.value for term in terms]
555+ self.assertEqual([], bug_trackers)
556+ self.assertRaises(
557+ LookupError, self.vocab.getTermByToken, 'marten')
558+
559+
560+def test_suite():
561+ return TestLoader().loadTestsFromName(__name__)
562
563=== modified file 'lib/lp/registry/doc/product-widgets.txt'
564--- lib/lp/registry/doc/product-widgets.txt 2010-06-15 03:11:34 +0000
565+++ lib/lp/registry/doc/product-widgets.txt 2010-06-17 19:17:25 +0000
566@@ -48,10 +48,13 @@
567 ... soup = BeautifulSoup(html)
568 ... labels = soup('label')
569 ... for label in labels:
570- ... if label.previous.previous.get('checked'):
571+ ... control = label.previous.previous
572+ ... if control['type'] == 'radio' and control.get('checked'):
573 ... print '[X]', extract_text(label)
574- ... else:
575+ ... elif control['type'] == 'radio':
576 ... print '[ ]', extract_text(label)
577+ ... else:
578+ ... pass
579 >>> print_items(widget())
580 [ ] In Launchpad
581 [ ] In a registered bug tracker:
582@@ -124,7 +127,7 @@
583
584 >>> form = {
585 ... 'field.bugtracker': 'malone',
586- ... 'field.bugtracker.bugtracker': '3',
587+ ... 'field.bugtracker.bugtracker': 'debbugs',
588 ... }
589 >>> widget = ProductBugTrackerWidget(
590 ... product_bugtracker, product_bugtracker.vocabulary,
591@@ -137,8 +140,6 @@
592 The bugtracker value passed to the widget caused the sub-widget used to select
593 the bug tracker to have the correct value.
594
595- >>> widget.bugtracker_widget.getInputValue().id
596- 3
597 >>> print widget.bugtracker_widget.getInputValue().name
598 debbugs
599
600@@ -210,6 +211,28 @@
601 >>> print firefox.bugtracker
602 None
603
604+The ProductBugTrackerWidget renders two fields that are subordinate to
605+the 4 choices.
606+
607+ >>> def print_controls(html):
608+ ... soup = BeautifulSoup(html)
609+ ... controls = soup('input')
610+ ... for control in controls:
611+ ... if control['type'] != 'hidden':
612+ ... if 'subordinate' in control.parent.get('class', ''):
613+ ... print '--'
614+ ... print control['id'], control['type']
615+
616+ >>> print_controls(widget())
617+ field.bugtracker.0 radio
618+ -- field.enable_bug_expiration checkbox
619+ field.bugtracker.2 radio
620+ field.bugtracker.bugtracker text
621+ -- field.remote_product text
622+ field.bugtracker.3 radio
623+ field.bugtracker.upstream_email_address text
624+ field.bugtracker.1 radio
625+
626
627 Choosing a License
628 ==================
629
630=== modified file 'lib/lp/registry/stories/project/xx-project-edit.txt'
631--- lib/lp/registry/stories/project/xx-project-edit.txt 2010-04-19 08:11:52 +0000
632+++ lib/lp/registry/stories/project/xx-project-edit.txt 2010-06-17 19:17:25 +0000
633@@ -16,7 +16,7 @@
634 >>> browser.getControl('Project Group Summary').value = 'New Summary.'
635 >>> browser.getControl('Description').value = 'New Description.'
636 >>> browser.getControl('Homepage URL').value = 'http://new-url.com/'
637- >>> browser.getControl(name='field.bugtracker').value = ['1']
638+ >>> browser.getControl(name='field.bugtracker').value = 'mozilla.org'
639 >>> browser.getControl('Change Details').click()
640
641 >>> browser.url