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
=== modified file 'lib/canonical/launchpad/icing/style-3-0.css.in'
--- lib/canonical/launchpad/icing/style-3-0.css.in 2010-06-11 18:39:35 +0000
+++ lib/canonical/launchpad/icing/style-3-0.css.in 2010-06-17 19:17:25 +0000
@@ -716,6 +716,12 @@
716form table label {716form table label {
717 font-weight: bold;717 font-weight: bold;
718 }718 }
719.compound {
720 margin-bottom: .5em;
721 }
722.field.subordinate label {
723 font-weight: normal;
724 }
719fieldset {725fieldset {
720 border-width: 2px 0 0;726 border-width: 2px 0 0;
721 margin: 1em 0;727 margin: 1em 0;
@@ -747,6 +753,9 @@
747.fieldRequired, .fieldOptional {753.fieldRequired, .fieldOptional {
748 color: #999;754 color: #999;
749 }755 }
756.field.subordinate {
757 margin-left: 2.6em;
758 }
750.formHelp {759.formHelp {
751 margin: 0.2em 0 0.5em 0.2em;760 margin: 0.2em 0 0.5em 0.2em;
752 color: #777;761 color: #777;
753762
=== modified file 'lib/canonical/launchpad/vocabularies/configure.zcml'
--- lib/canonical/launchpad/vocabularies/configure.zcml 2010-01-11 18:59:53 +0000
+++ lib/canonical/launchpad/vocabularies/configure.zcml 2010-06-17 19:17:25 +0000
@@ -95,6 +95,18 @@
9595
9696
97 <securedutility97 <securedutility
98 name="WebBugTracker"
99 component="canonical.launchpad.vocabularies.WebBugTrackerVocabulary"
100 provides="zope.schema.interfaces.IVocabularyFactory"
101 >
102 <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
103 </securedutility>
104
105 <class class="canonical.launchpad.vocabularies.WebBugTrackerVocabulary">
106 <allow interface="canonical.launchpad.webapp.vocabulary.IHugeVocabulary"/>
107 </class>
108
109 <securedutility
98 name="BugWatch"110 name="BugWatch"
99 component="canonical.launchpad.vocabularies.BugWatchVocabulary"111 component="canonical.launchpad.vocabularies.BugWatchVocabulary"
100 provides="zope.schema.interfaces.IVocabularyFactory"112 provides="zope.schema.interfaces.IVocabularyFactory"
101113
=== 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-17 19:17:25 +0000
@@ -47,13 +47,16 @@
47import cgi47import cgi
48from operator import attrgetter48from operator import attrgetter
4949
50from sqlobject import AND, SQLObjectNotFound50from sqlobject import AND, CONTAINSSTRING, SQLObjectNotFound
51from storm.expr import SQL51from storm.expr import SQL
52from zope.component import getUtility52from zope.component import getUtility
53from zope.interface import implements53from zope.interface import implements
54from zope.schema.interfaces import IVocabulary, IVocabularyTokenized54from zope.schema.interfaces import IVocabulary, IVocabularyTokenized
55from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary55from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
5656
57from storm.expr import And, Or
58
59from canonical.launchpad.interfaces.lpstorm import IStore
57from lp.code.model.branch import Branch60from lp.code.model.branch import Branch
58from lp.bugs.model.bug import Bug61from lp.bugs.model.bug import Bug
59from lp.bugs.model.bugtracker import BugTracker62from lp.bugs.model.bugtracker import BugTracker
@@ -226,15 +229,52 @@
226229
227230
228class BugTrackerVocabulary(SQLObjectVocabularyBase):231class BugTrackerVocabulary(SQLObjectVocabularyBase):
229232 """All web and email based external bug trackers."""
233 displayname = 'Select a bug tracker'
234 implements(IHugeVocabulary)
230 _table = BugTracker235 _table = BugTracker
236 _filter = True
231 _orderBy = 'title'237 _orderBy = 'title'
238 _order_by = [BugTracker.title]
239
240 def toTerm(self, obj):
241 """See `IVocabulary`."""
242 return SimpleTerm(obj, obj.name, obj.title)
243
244 def getTermByToken(self, token):
245 """See `IVocabularyTokenized`."""
246 result = IStore(self._table).find(
247 self._table,
248 self._filter,
249 BugTracker.name == token).one()
250 if result is None:
251 raise LookupError(token)
252 return self.toTerm(result)
253
254 def search(self, query):
255 """Search for web bug trackers."""
256 query = query.lower()
257 results = IStore(self._table).find(
258 self._table, And(
259 self._filter,
260 BugTracker.active == True,
261 Or(
262 CONTAINSSTRING(BugTracker.name, query),
263 CONTAINSSTRING(BugTracker.title, query),
264 CONTAINSSTRING(BugTracker.summary, query),
265 CONTAINSSTRING(BugTracker.baseurl, query))))
266 results = results.order_by(self._order_by)
267 return results
268
269 def searchForTerms(self, query=None):
270 """See `IHugeVocabulary`."""
271 results = self.search(query)
272 return CountableIterator(results.count(), results, self.toTerm)
232273
233274
234class WebBugTrackerVocabulary(BugTrackerVocabulary):275class WebBugTrackerVocabulary(BugTrackerVocabulary):
235 """All web-based bug tracker types."""276 """All web-based bug tracker types."""
236277 _filter = BugTracker.bugtrackertype != BugTrackerType.EMAILADDRESS
237 _filter = BugTracker.q.bugtrackertype != BugTrackerType.EMAILADDRESS
238278
239279
240class LanguageVocabulary(SQLObjectVocabularyBase):280class LanguageVocabulary(SQLObjectVocabularyBase):
241281
=== modified file 'lib/canonical/widgets/product.py'
--- lib/canonical/widgets/product.py 2010-06-15 03:08:22 +0000
+++ lib/canonical/widgets/product.py 2010-06-17 19:17:25 +0000
@@ -5,6 +5,7 @@
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'GhostCheckBoxWidget',
8 'GhostWidget',9 'GhostWidget',
9 'LicenseWidget',10 'LicenseWidget',
10 'ProductBugTrackerWidget',11 'ProductBugTrackerWidget',
@@ -15,6 +16,7 @@
15import math16import math
1617
17from zope.app.form import CustomWidgetFactory18from zope.app.form import CustomWidgetFactory
19from zope.app.form.browser.boolwidgets import CheckBoxWidget
18from zope.app.form.browser.textwidgets import TextWidget20from zope.app.form.browser.textwidgets import TextWidget
19from zope.app.form.browser.widget import renderElement21from zope.app.form.browser.widget import renderElement
20from zope.app.form.interfaces import IInputWidget22from zope.app.form.interfaces import IInputWidget
@@ -24,25 +26,28 @@
2426
25from z3c.ptcompat import ViewPageTemplateFile27from z3c.ptcompat import ViewPageTemplateFile
2628
29from lazr.restful.interface import copy_field
30
27from canonical.launchpad.browser.widgets import DescriptionWidget31from canonical.launchpad.browser.widgets import DescriptionWidget
28from canonical.launchpad.fields import StrippedTextLine32from canonical.launchpad.fields import StrippedTextLine
29from canonical.launchpad.interfaces import (33from canonical.launchpad.interfaces import (
30 BugTrackerType, IBugTracker, IBugTrackerSet, ILaunchBag)34 BugTrackerType, IBugTracker, IBugTrackerSet, ILaunchBag)
31from canonical.launchpad.validators import LaunchpadValidationError35from canonical.launchpad.validators import LaunchpadValidationError
32from canonical.launchpad.validators.email import email_validator36from canonical.launchpad.validators.email import email_validator
33from canonical.launchpad.vocabularies.dbobjects import (
34 WebBugTrackerVocabulary)
35from canonical.launchpad.webapp import canonical_url37from canonical.launchpad.webapp import canonical_url
36from canonical.widgets.itemswidgets import (38from canonical.widgets.itemswidgets import (
37 CheckBoxMatrixWidget, LaunchpadDropdownWidget, LaunchpadRadioWidget)39 CheckBoxMatrixWidget, LaunchpadRadioWidget)
40from canonical.widgets.popup import VocabularyPickerWidget
38from canonical.widgets.textwidgets import (41from canonical.widgets.textwidgets import (
39 LowerCaseTextWidget, StrippedTextWidget)42 LowerCaseTextWidget, StrippedTextWidget)
43from lp.registry.interfaces.product import IProduct
4044
4145
42class ProductBugTrackerWidget(LaunchpadRadioWidget):46class ProductBugTrackerWidget(LaunchpadRadioWidget):
43 """Widget for selecting a product bug tracker."""47 """Widget for selecting a product bug tracker."""
4448
45 _joinButtonToMessageTemplate = u'%s&nbsp;%s'49 _joinButtonToMessageTemplate = u'%s&nbsp;%s'
50 template = ViewPageTemplateFile('templates/product-bug-tracker.pt')
4651
47 def __init__(self, field, vocabulary, request):52 def __init__(self, field, vocabulary, request):
48 # pylint: disable-msg=W023353 # pylint: disable-msg=W0233
@@ -50,19 +55,15 @@
5055
51 # Bug tracker widget.56 # Bug tracker widget.
52 self.bugtracker = Choice(57 self.bugtracker = Choice(
53 vocabulary=WebBugTrackerVocabulary(),58 vocabulary="WebBugTracker",
54 __name__='bugtracker')59 __name__='bugtracker')
55 self.bugtracker_widget = CustomWidgetFactory(LaunchpadDropdownWidget)60 self.bugtracker_widget = CustomWidgetFactory(VocabularyPickerWidget)
56 setUpWidget(61 setUpWidget(
57 self, 'bugtracker', self.bugtracker, IInputWidget,62 self, 'bugtracker', self.bugtracker, IInputWidget,
58 prefix=self.name, value=field.context.bugtracker,63 prefix=self.name, value=field.context.bugtracker,
59 context=field.context)64 context=field.context)
60 if self.bugtracker_widget.extra is None:65 self.bugtracker_widget.onKeyPress = (
61 self.bugtracker_widget.extra = ''66 "selectWidget('%s.2', event);" % self.name)
62 ## Select the corresponding radio option automatically if
63 ## the user selects a bug tracker.
64 self.bugtracker_widget.extra += (
65 ' onchange="selectWidget(\'%s.2\', event);"' % self.name)
6667
67 # Upstream email address field and widget.68 # Upstream email address field and widget.
68 ## This is to make email address bug trackers appear69 ## This is to make email address bug trackers appear
@@ -198,10 +199,12 @@
198 value="external-email", name=self.name, cssClass=self.cssClass)199 value="external-email", name=self.name, cssClass=self.cssClass)
199200
200 # All the choices arguments in order.201 # All the choices arguments in order.
201 all_arguments = [malone_item_arguments,202 all_arguments = {
202 external_bugtracker_arguments,203 'launchpad': malone_item_arguments,
203 external_bugtracker_email_arguments,204 'external_bugtracker': external_bugtracker_arguments,
204 project_bugtracker_arguments]205 'external_email': external_bugtracker_email_arguments,
206 'unknown': project_bugtracker_arguments,
207 }
205208
206 # Figure out the selected choice.209 # Figure out the selected choice.
207 if value == field.malone_marker:210 if value == field.malone_marker:
@@ -219,12 +222,35 @@
219 selected = project_bugtracker_arguments222 selected = project_bugtracker_arguments
220223
221 # Render.224 # Render.
222 for arguments in all_arguments:225 for name, arguments in all_arguments.items():
223 if arguments is selected:226 if arguments is selected:
224 render = self.renderSelectedItem227 render = self.renderSelectedItem
225 else:228 else:
226 render = self.renderItem229 render = self.renderItem
227 yield render(**arguments)230 yield (name, render(**arguments))
231
232 def renderValue(self, value):
233 # Render the items with subordinate fields and support markup.
234 self.bug_trackers = dict(self.renderItems(value))
235 self.product = self.context.context
236 # The view must also use GhostWidget for the 'remote_product' field.
237 self.remote_product = copy_field(IProduct['remote_product'])
238 self.remote_product_widget = CustomWidgetFactory(TextWidget)
239 setUpWidget(
240 self, 'remote_product', self.remote_product, IInputWidget,
241 prefix='field', value=self.product.remote_product,
242 context=self.product)
243 # The view must also use GhostWidget for the 'enable_bug_expiration'
244 # field.
245 self.enable_bug_expiration = copy_field(
246 IProduct['enable_bug_expiration'])
247 self.enable_bug_expiration_widget = CustomWidgetFactory(
248 CheckBoxWidget)
249 setUpWidget(
250 self, 'enable_bug_expiration', self.enable_bug_expiration,
251 IInputWidget, prefix='field',
252 value=self.product.enable_bug_expiration, context=self.product)
253 return self.template()
228254
229255
230class LicenseWidget(CheckBoxMatrixWidget):256class LicenseWidget(CheckBoxMatrixWidget):
@@ -406,7 +432,7 @@
406 return 'text'432 return 'text'
407433
408434
409class GhostWidget(TextWidget):435class GhostMixin:
410 """A simple widget that has no HTML."""436 """A simple widget that has no HTML."""
411 visible = False437 visible = False
412 # This suppresses the stuff above the widget.438 # This suppresses the stuff above the widget.
@@ -420,3 +446,11 @@
420 return ''446 return ''
421447
422 hidden = __call__448 hidden = __call__
449
450
451class GhostWidget(GhostMixin, TextWidget):
452 """Suppress the rendering of Text input fields."""
453
454
455class GhostCheckBoxWidget(GhostMixin, CheckBoxWidget):
456 """Suppress the rendering of Bool input fields."""
423457
=== added file 'lib/canonical/widgets/templates/product-bug-tracker.pt'
--- lib/canonical/widgets/templates/product-bug-tracker.pt 1970-01-01 00:00:00 +0000
+++ lib/canonical/widgets/templates/product-bug-tracker.pt 2010-06-17 19:17:25 +0000
@@ -0,0 +1,24 @@
1<tal:root
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 omit-tag="">
6 <div class="compound">
7 <div tal:content="structure view/bug_trackers/launchpad" />
8 <div class="field subordinate">
9 <input tal:replace="structure view/enable_bug_expiration_widget" />
10 <label
11 tal:attributes="for view/enable_bug_expiration_widget/name"
12 tal:content="view/enable_bug_expiration/title" />
13 </div>
14 </div>
15 <div class="compound">
16 <div tal:content="structure view/bug_trackers/external_bugtracker" />
17 <div class="field subordinate">
18 Project ID in bug tracker:
19 <input tal:replace="structure view/remote_product_widget" />
20 </div>
21 </div>
22 <div class="compound" tal:content="structure view/bug_trackers/external_email" />
23 <div class="compound" tal:content="structure view/bug_trackers/unknown" />
24</tal:root>
025
=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py 2010-06-15 11:35:12 +0000
+++ lib/lp/bugs/browser/bugtarget.py 2010-06-17 19:17:25 +0000
@@ -98,7 +98,8 @@
98from canonical.launchpad.webapp.publisher import HTTP_MOVED_PERMANENTLY98from canonical.launchpad.webapp.publisher import HTTP_MOVED_PERMANENTLY
99from canonical.widgets.bug import BugTagsWidget, LargeBugTagsWidget99from canonical.widgets.bug import BugTagsWidget, LargeBugTagsWidget
100from canonical.widgets.bugtask import NewLineToSpacesWidget100from canonical.widgets.bugtask import NewLineToSpacesWidget
101from canonical.widgets.product import ProductBugTrackerWidget101from canonical.widgets.product import (
102 ProductBugTrackerWidget, GhostCheckBoxWidget, GhostWidget)
102from lp.registry.vocabularies import ValidPersonOrTeamVocabulary103from lp.registry.vocabularies import ValidPersonOrTeamVocabulary
103104
104105
@@ -145,7 +146,11 @@
145 "bug_reporting_guidelines",146 "bug_reporting_guidelines",
146 "bug_reported_acknowledgement",147 "bug_reported_acknowledgement",
147 ]148 ]
149 # This ProductBugTrackerWidget renders enable_bug_expiration and
150 # remote_product as subordinate fields, so this view suppresses them.
148 custom_widget('bugtracker', ProductBugTrackerWidget)151 custom_widget('bugtracker', ProductBugTrackerWidget)
152 custom_widget('enable_bug_expiration', GhostCheckBoxWidget)
153 custom_widget('remote_product', GhostWidget)
149154
150 def validate(self, data):155 def validate(self, data):
151 """Constrain bug expiration to Launchpad Bugs tracker."""156 """Constrain bug expiration to Launchpad Bugs tracker."""
152157
=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_configure.py'
--- lib/lp/bugs/browser/tests/test_bugtarget_configure.py 2010-06-14 15:52:41 +0000
+++ lib/lp/bugs/browser/tests/test_bugtarget_configure.py 2010-06-17 19:17:25 +0000
@@ -105,7 +105,7 @@
105 form = self._makeForm()105 form = self._makeForm()
106 form['field.enable_bug_expiration'] = 'on'106 form['field.enable_bug_expiration'] = 'on'
107 form['field.bugtracker'] = 'external'107 form['field.bugtracker'] = 'external'
108 form['field.bugtracker.bugtracker'] = '3'108 form['field.bugtracker.bugtracker'] = 'debbugs'
109 view = create_initialized_view(109 view = create_initialized_view(
110 self.product, name='+configure-bugtracker', form=form)110 self.product, name='+configure-bugtracker', form=form)
111 self.assertEqual([], view.errors)111 self.assertEqual([], view.errors)
112112
=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-upstream-bugtracker-links.txt'
--- lib/lp/bugs/stories/bug-also-affects/xx-upstream-bugtracker-links.txt 2010-06-16 16:47:54 +0000
+++ lib/lp/bugs/stories/bug-also-affects/xx-upstream-bugtracker-links.txt 2010-06-17 19:17:25 +0000
@@ -33,7 +33,7 @@
33 >>> admin_browser.getControl(33 >>> admin_browser.getControl(
34 ... name='field.bugtracker').value = ['external']34 ... name='field.bugtracker').value = ['external']
35 >>> admin_browser.getControl(35 >>> admin_browser.getControl(
36 ... name='field.bugtracker.bugtracker').value = ['1']36 ... name='field.bugtracker.bugtracker').value = 'mozilla.org'
37 >>> admin_browser.getControl('Change').click()37 >>> admin_browser.getControl('Change').click()
3838
39 >>> user_browser.open('http://launchpad.dev/bugs/13/')39 >>> user_browser.open('http://launchpad.dev/bugs/13/')
@@ -95,8 +95,7 @@
95 >>> admin_browser.getControl(95 >>> admin_browser.getControl(
96 ... 'In a registered bug tracker:').selected = True96 ... 'In a registered bug tracker:').selected = True
97 >>> admin_browser.getControl(97 >>> admin_browser.getControl(
98 ... name='field.bugtracker.bugtracker').displayValue = [98 ... name='field.bugtracker.bugtracker').value = 'debbugs'
99 ... 'Debian Bug tracker']
100 >>> admin_browser.getControl('Change').click()99 >>> admin_browser.getControl('Change').click()
101100
102 >>> user_browser.open('http://launchpad.dev/bugs/13/')101 >>> user_browser.open('http://launchpad.dev/bugs/13/')
@@ -122,11 +121,11 @@
122121
123 >>> admin_browser.open(122 >>> admin_browser.open(
124 ... 'http://launchpad.dev/thunderbird/+configure-bugtracker')123 ... 'http://launchpad.dev/thunderbird/+configure-bugtracker')
125 >>> admin_browser.getControl('Remote bug tracker project id').value = (124 >>> admin_browser.getControl(name='field.remote_product').value = (
126 ... 'Thunderbird')125 ... 'Thunderbird')
127 >>> admin_browser.getControl('Change').click()126 >>> admin_browser.getControl('Change').click()
128127
129 >>> admin_browser.open(128 >>> admin_browser.open(
130 ... 'http://launchpad.dev/thunderbird/+configure-bugtracker')129 ... 'http://launchpad.dev/thunderbird/+configure-bugtracker')
131 >>> print admin_browser.getControl('Remote bug tracker project id').value130 >>> print admin_browser.getControl(name='field.remote_product').value
132 Thunderbird131 Thunderbird
133132
=== modified file 'lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt'
--- lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt 2010-05-19 05:47:50 +0000
+++ lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt 2010-06-17 19:17:25 +0000
@@ -92,7 +92,7 @@
92 ... "http://launchpad.dev/%s/+configure-bugtracker" % name)92 ... "http://launchpad.dev/%s/+configure-bugtracker" % name)
93 ... admin_browser.getControl("In a registered bug tracker").click()93 ... admin_browser.getControl("In a registered bug tracker").click()
94 ... bt = admin_browser.getControl(name="field.bugtracker.bugtracker")94 ... bt = admin_browser.getControl(name="field.bugtracker.bugtracker")
95 ... bt.value = ["3"]95 ... bt.value = 'debbugs'
96 ... admin_browser.getControl("Change").click()96 ... admin_browser.getControl("Change").click()
97 ...97 ...
98 >>> link_to_debbugs('upstart')98 >>> link_to_debbugs('upstart')
9999
=== modified file 'lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt'
--- lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt 2010-04-09 19:11:36 +0000
+++ lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt 2010-06-17 19:17:25 +0000
@@ -668,8 +668,7 @@
668bug tracker:668bug tracker:
669669
670 >>> admin_browser.open('http://launchpad.dev/mozilla/+edit')670 >>> admin_browser.open('http://launchpad.dev/mozilla/+edit')
671 >>> admin_browser.getControl('Bug Tracker:').displayValue = [671 >>> admin_browser.getControl('Bug Tracker:').value = 'testbugzilla'
672 ... 'A test Bugzilla Tracker']
673 >>> admin_browser.getControl('Change Details').click()672 >>> admin_browser.getControl('Change Details').click()
674673
675 >>> admin_browser.open(674 >>> admin_browser.open(
@@ -677,7 +676,7 @@
677 >>> admin_browser.getControl(name='field.bugtracker'676 >>> admin_browser.getControl(name='field.bugtracker'
678 ... ).displayValue = ['In a registered bug tracker:']677 ... ).displayValue = ['In a registered bug tracker:']
679 >>> admin_browser.getControl(name='field.bugtracker.bugtracker'678 >>> admin_browser.getControl(name='field.bugtracker.bugtracker'
680 ... ).displayValue = ['A test Bugzilla Tracker']679 ... ).value = 'testbugzilla'
681 >>> admin_browser.getControl('Change').click()680 >>> admin_browser.getControl('Change').click()
682681
683Now the Mozilla Project and Jokosher will appear in the Related682Now the Mozilla Project and Jokosher will appear in the Related
684683
=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt 2010-04-16 15:06:55 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt 2010-06-17 19:17:25 +0000
@@ -220,7 +220,7 @@
220 >>> admin_browser.open('http://launchpad.dev/testy/+configure-bugtracker')220 >>> admin_browser.open('http://launchpad.dev/testy/+configure-bugtracker')
221 >>> admin_browser.getControl(name='field.bugtracker').value = ['external']221 >>> admin_browser.getControl(name='field.bugtracker').value = ['external']
222 >>> admin_browser.getControl(222 >>> admin_browser.getControl(
223 ... name='field.bugtracker.bugtracker').value = ['3']223 ... name='field.bugtracker.bugtracker').value = 'debbugs'
224 >>> admin_browser.getControl('Change').click()224 >>> admin_browser.getControl('Change').click()
225225
226And on the filebug page...226And on the filebug page...
227227
=== added file 'lib/lp/bugs/tests/test_bugtracker_vocabulary.py'
--- lib/lp/bugs/tests/test_bugtracker_vocabulary.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/tests/test_bugtracker_vocabulary.py 2010-06-17 19:17:25 +0000
@@ -0,0 +1,123 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test the bug tracker vocabularies."""
5
6__metaclass__ = type
7
8from unittest import TestLoader
9
10from zope.schema.vocabulary import getVocabularyRegistry
11
12from canonical.launchpad.ftests import login, login_person
13from canonical.testing import DatabaseFunctionalLayer
14from lp.bugs.interfaces.bugtracker import BugTrackerType
15from lp.testing import TestCaseWithFactory
16
17
18class TestBugTrackerVocabulary(TestCaseWithFactory):
19
20 layer = DatabaseFunctionalLayer
21
22 def setUp(self):
23 super(TestBugTrackerVocabulary, self).setUp()
24 vocabulary_registry = getVocabularyRegistry()
25 self.vocab = vocabulary_registry.get(None, 'BugTracker')
26
27 def test_toTerm(self):
28 # Verify the data in the term.
29 bug_tracker = self.factory.makeBugTracker()
30 login_person(bug_tracker.owner)
31 bug_tracker.name = 'weasel'
32 bug_tracker.title = 'The Weasel Bug Tracker'
33 [term] = self.vocab.searchForTerms('weasel')
34 self.assertEqual(bug_tracker, term.value)
35 self.assertEqual(bug_tracker.name, term.token)
36 self.assertEqual(bug_tracker.title, term.title)
37
38 def test_getTermByToken_match(self):
39 # Verify the token by lookup.
40 bug_tracker = self.factory.makeBugTracker()
41 login_person(bug_tracker.owner)
42 bug_tracker.name = 'mink'
43 term = self.vocab.getTermByToken('mink')
44 self.assertEqual(bug_tracker, term.value)
45
46 def test_getTermByToken_no_match(self):
47 # Verify that a LookupError error is raised.
48 self.assertRaises(
49 LookupError, self.vocab.getTermByToken, 'does not exist')
50
51 def searchForBugTrackers(self, query):
52 terms = self.vocab.searchForTerms(query)
53 return [term.value for term in terms]
54
55 def test_search_name(self):
56 # Verify that queries match name text.
57 bug_tracker = self.factory.makeBugTracker()
58 login_person(bug_tracker.owner)
59 bug_tracker.name = 'skunkworks'
60 bug_trackers = self.searchForBugTrackers('skunk')
61 self.assertEqual([bug_tracker], bug_trackers)
62
63 def test_search_title(self):
64 # Verify that queries match title text.
65 bug_tracker = self.factory.makeBugTracker()
66 login_person(bug_tracker.owner)
67 bug_tracker.title = 'A ferret in your pants'
68 bug_trackers = self.searchForBugTrackers('ferret')
69 self.assertEqual([bug_tracker], bug_trackers)
70
71 def test_search_summary(self):
72 # Verify that queries match summary text.
73 bug_tracker = self.factory.makeBugTracker()
74 login_person(bug_tracker.owner)
75 bug_tracker.summary = 'A badger is a member of the weasel family.'
76 bug_trackers = self.searchForBugTrackers('badger')
77 self.assertEqual([bug_tracker], bug_trackers)
78
79 def test_search_baseurl(self):
80 # Verify that queries match baseurl text.
81 bug_tracker = self.factory.makeBugTracker(
82 base_url='http://bugs.otter.dom/')
83 bug_trackers = self.searchForBugTrackers('otter')
84 self.assertEqual([bug_tracker], bug_trackers)
85
86 def test_search_inactive(self):
87 # Verify that inactive bug trackers are not returned by search,
88 # but are in the vocabulary.
89 bug_tracker = self.factory.makeBugTracker()
90 login('admin@canonical.com')
91 bug_tracker.name = 'stoat'
92 bug_tracker.active = False
93 bug_trackers = self.searchForBugTrackers('stoat')
94 self.assertEqual([], bug_trackers)
95 term = self.vocab.getTermByToken('stoat')
96 self.assertEqual(bug_tracker, term.value)
97
98
99class TestWebBugTrackerVocabulary(TestCaseWithFactory):
100
101 layer = DatabaseFunctionalLayer
102
103 def setUp(self):
104 super(TestWebBugTrackerVocabulary, self).setUp()
105 vocabulary_registry = getVocabularyRegistry()
106 self.vocab = vocabulary_registry.get(None, 'WebBugTracker')
107
108 def test_search_no_email_type(self):
109 # Verify that emailaddress bug trackers are not returned by search,
110 # and are not in the vocabulary.
111 bug_tracker = self.factory.makeBugTracker(
112 bugtrackertype=BugTrackerType.EMAILADDRESS)
113 login_person(bug_tracker.owner)
114 bug_tracker.name = 'marten'
115 terms = self.vocab.searchForTerms('marten')
116 bug_trackers = [term.value for term in terms]
117 self.assertEqual([], bug_trackers)
118 self.assertRaises(
119 LookupError, self.vocab.getTermByToken, 'marten')
120
121
122def test_suite():
123 return TestLoader().loadTestsFromName(__name__)
0124
=== modified file 'lib/lp/registry/doc/product-widgets.txt'
--- lib/lp/registry/doc/product-widgets.txt 2010-06-15 03:11:34 +0000
+++ lib/lp/registry/doc/product-widgets.txt 2010-06-17 19:17:25 +0000
@@ -48,10 +48,13 @@
48 ... soup = BeautifulSoup(html)48 ... soup = BeautifulSoup(html)
49 ... labels = soup('label')49 ... labels = soup('label')
50 ... for label in labels:50 ... for label in labels:
51 ... if label.previous.previous.get('checked'):51 ... control = label.previous.previous
52 ... if control['type'] == 'radio' and control.get('checked'):
52 ... print '[X]', extract_text(label)53 ... print '[X]', extract_text(label)
53 ... else:54 ... elif control['type'] == 'radio':
54 ... print '[ ]', extract_text(label)55 ... print '[ ]', extract_text(label)
56 ... else:
57 ... pass
55 >>> print_items(widget())58 >>> print_items(widget())
56 [ ] In Launchpad59 [ ] In Launchpad
57 [ ] In a registered bug tracker:60 [ ] In a registered bug tracker:
@@ -124,7 +127,7 @@
124127
125 >>> form = {128 >>> form = {
126 ... 'field.bugtracker': 'malone',129 ... 'field.bugtracker': 'malone',
127 ... 'field.bugtracker.bugtracker': '3',130 ... 'field.bugtracker.bugtracker': 'debbugs',
128 ... }131 ... }
129 >>> widget = ProductBugTrackerWidget(132 >>> widget = ProductBugTrackerWidget(
130 ... product_bugtracker, product_bugtracker.vocabulary,133 ... product_bugtracker, product_bugtracker.vocabulary,
@@ -137,8 +140,6 @@
137The bugtracker value passed to the widget caused the sub-widget used to select140The bugtracker value passed to the widget caused the sub-widget used to select
138the bug tracker to have the correct value.141the bug tracker to have the correct value.
139142
140 >>> widget.bugtracker_widget.getInputValue().id
141 3
142 >>> print widget.bugtracker_widget.getInputValue().name143 >>> print widget.bugtracker_widget.getInputValue().name
143 debbugs144 debbugs
144145
@@ -210,6 +211,28 @@
210 >>> print firefox.bugtracker211 >>> print firefox.bugtracker
211 None212 None
212213
214The ProductBugTrackerWidget renders two fields that are subordinate to
215the 4 choices.
216
217 >>> def print_controls(html):
218 ... soup = BeautifulSoup(html)
219 ... controls = soup('input')
220 ... for control in controls:
221 ... if control['type'] != 'hidden':
222 ... if 'subordinate' in control.parent.get('class', ''):
223 ... print '--'
224 ... print control['id'], control['type']
225
226 >>> print_controls(widget())
227 field.bugtracker.0 radio
228 -- field.enable_bug_expiration checkbox
229 field.bugtracker.2 radio
230 field.bugtracker.bugtracker text
231 -- field.remote_product text
232 field.bugtracker.3 radio
233 field.bugtracker.upstream_email_address text
234 field.bugtracker.1 radio
235
213236
214Choosing a License237Choosing a License
215==================238==================
216239
=== modified file 'lib/lp/registry/stories/project/xx-project-edit.txt'
--- lib/lp/registry/stories/project/xx-project-edit.txt 2010-04-19 08:11:52 +0000
+++ lib/lp/registry/stories/project/xx-project-edit.txt 2010-06-17 19:17:25 +0000
@@ -16,7 +16,7 @@
16 >>> browser.getControl('Project Group Summary').value = 'New Summary.'16 >>> browser.getControl('Project Group Summary').value = 'New Summary.'
17 >>> browser.getControl('Description').value = 'New Description.'17 >>> browser.getControl('Description').value = 'New Description.'
18 >>> browser.getControl('Homepage URL').value = 'http://new-url.com/'18 >>> browser.getControl('Homepage URL').value = 'http://new-url.com/'
19 >>> browser.getControl(name='field.bugtracker').value = ['1']19 >>> browser.getControl(name='field.bugtracker').value = 'mozilla.org'
20 >>> browser.getControl('Change Details').click()20 >>> browser.getControl('Change Details').click()
2121
22 >>> browser.url22 >>> browser.url