launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #02546
[Merge] lp:~sinzui/launchpad/recipe-owner-widget-1 into lp:launchpad
Curtis Hovey has proposed merging lp:~sinzui/launchpad/recipe-owner-widget-1 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
#714527 Owner widget on Branch:+new-recipe doesn't render structured strings properly
https://bugs.launchpad.net/bugs/714527
For more details, see:
https://code.launchpad.net/~sinzui/launchpad/recipe-owner-widget-1/+merge/48937
Properly escape labels in suggestion widgets
Launchpad bug: https://bugs.launchpad.net/bugs/714527
Pre-implementation: no one
Test command: ./bin/test -vv \
-t test_suggestion -t test_itemwidgets
On a page like https://code.launchpad.net/~launchpad-pqm/launchpad/devel/+new-
recipe, the Owner widget has HTML like '<structured-string'. It clearly isn't
being printed properly, but the contents seem to be fine.
ADDENDUM: While writing the test to see the problem, I discovered that
all SuggestionWidgets render invalid HTML markup. They create nested
<labels>.
--------------------------------------------------------------------
RULES
* Update renderItems() to escape() the call to self._renderLabel()
to get the IStructured.escapedtext value.
* Redefine _renderItem() to so that a wrapping label is not added,
or make _renderItem() in the base class smart enough know not to
add a wrapping label if the text contains <label.
QA
* Visit https://code.qastaging.launchpad.net/~launchpad-pqm/launchpad/devel/+new-recipe
* Verify the owner widget renders the Other label element by clicking the
text and see that the item is selected.
* View the source/inspector and verify that labels are not nested.
LINT
lib/lp/app/widgets/itemswidgets.py
lib/lp/app/widgets/suggestion.py
lib/lp/app/widgets/tests/test_itemswidgets.py
lib/lp/app/widgets/tests/test_suggestion.py
IMPLEMENTATION
Added a rule to omit the wrapping label if the text already has a label. I
tried the simple approach of redefining _renderItem() in SuggestionWidget,
but the code did not look DRY, and the tests were repeated. I decided that
making the base class smarter would prevent extra work for other subclasses.
lib/lp/app/widgets/itemswidgets.py
lib/lp/app/widgets/tests/test_itemswidgets.py
I updated the rule that makes the label to only escape the passed text, then
uses escape() to get the IStructured.escapedtext. Setting up a test for the
base widget class was a lot of work for what is actually a two line fix.
Added future tests will be easy :)
lib/lp/app/widgets/suggestion.py
lib/lp/app/widgets/tests/test_suggestion.py
--
https://code.launchpad.net/~sinzui/launchpad/recipe-owner-widget-1/+merge/48937
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/recipe-owner-widget-1 into lp:launchpad.
=== modified file 'lib/lp/app/widgets/itemswidgets.py'
--- lib/lp/app/widgets/itemswidgets.py 2011-02-03 22:52:06 +0000
+++ lib/lp/app/widgets/itemswidgets.py 2011-02-08 15:57:57 +0000
@@ -110,9 +110,12 @@
cssClass=cssClass,
type='radio',
**kw)
- return renderElement(u'label',
- contents='%s %s' % (elem, text),
- **{'style': 'font-weight: normal'})
+ if '<label' in text:
+ return '%s %s' % (elem, text)
+ else:
+ return renderElement(u'label',
+ contents='%s %s' % (elem, text),
+ **{'style': 'font-weight: normal'})
def _div(self, cssClass, contents, **kw):
return contents
=== modified file 'lib/lp/app/widgets/suggestion.py'
--- lib/lp/app/widgets/suggestion.py 2011-02-03 22:52:06 +0000
+++ lib/lp/app/widgets/suggestion.py 2011-02-08 15:57:57 +0000
@@ -136,9 +136,10 @@
def _renderLabel(self, text, index):
"""Render a label for the option with the specified index."""
- label = u'<label for="%s" style="font-weight: normal">%s</label>' % (
+ label = structured(
+ u'<label for="%s" style="font-weight: normal">%s</label>',
self._optionId(index), text)
- return structured(label)
+ return label
def _renderSuggestionLabel(self, value, index):
"""Render a label for the option based on a branch."""
@@ -173,7 +174,7 @@
# Lastly render the other option.
index = len(items)
other_selection_text = "%s %s" % (
- self._renderLabel("Other:", index),
+ escape(self._renderLabel("Other:", index)),
self.other_selection_widget())
other_selection_onclick = (
"this.form['%s'].focus()" % self.other_selection_widget.name)
=== modified file 'lib/lp/app/widgets/tests/test_itemswidgets.py'
--- lib/lp/app/widgets/tests/test_itemswidgets.py 2011-02-03 16:50:09 +0000
+++ lib/lp/app/widgets/tests/test_itemswidgets.py 2011-02-08 15:57:57 +0000
@@ -18,6 +18,7 @@
Item,
)
+from canonical.launchpad.webapp.menu import structured
from canonical.launchpad.webapp.servers import LaunchpadTestRequest
from canonical.testing.layers import DatabaseFunctionalLayer
from lp.app.widgets.itemswidgets import (
@@ -136,6 +137,13 @@
' <unsafe> &nbsp; title</label>')
self.assertRenderItem(expected, self.UNSAFE_TERM, checked=False)
+ def test__renderItem_without_label(self):
+ # Render item omits a wrapping label if the text contains a label.
+ html_term = SimpleTerm(
+ 'object-3', 'token-3', structured('<label>title</label>'))
+ expected = ('<input ... /> <label>title</label>')
+ self.assertRenderItem(expected, html_term, checked=False)
+
class TestLaunchpadRadioWidgetWithDescription(TestCaseWithFactory):
"""Test the LaunchpadRadioWidgetWithDescription class."""
=== modified file 'lib/lp/app/widgets/tests/test_suggestion.py'
--- lib/lp/app/widgets/tests/test_suggestion.py 2011-02-01 21:08:27 +0000
+++ lib/lp/app/widgets/tests/test_suggestion.py 2011-02-08 15:57:57 +0000
@@ -8,20 +8,114 @@
datetime,
timedelta,
)
+import doctest
from pytz import utc
+
+from zope.component import provideUtility
+from zope.interface import implements
from zope.schema import Choice
+from zope.schema.interfaces import IVocabularyFactory
+from zope.schema.vocabulary import (
+ SimpleTerm,
+ SimpleVocabulary,
+ )
from zope.security.proxy import removeSecurityProxy
+from testtools.matchers import DocTestMatches
+
from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+from canonical.launchpad.webapp.vocabulary import IHugeVocabulary
from canonical.testing.layers import DatabaseFunctionalLayer
-from lp.app.widgets.suggestion import TargetBranchWidget
+from lp.app.widgets.suggestion import (
+ SuggestionWidget,
+ TargetBranchWidget,
+ )
from lp.testing import (
person_logged_in,
TestCaseWithFactory,
)
+class Simple:
+ """A simple class to test fields ans widgets."""
+
+ def __init__(self, name, displayname):
+ self.name = name
+ self.displayname = displayname
+
+
+class SimpleHugeVocabulary(SimpleVocabulary):
+ implements(IHugeVocabulary)
+ displayname = "Simple objects"
+ step_title = "Select something"
+
+ def __call__(self, context):
+ # Allow an instance to be used as a utility.
+ return self
+
+
+class TestSuggestionWidget(TestCaseWithFactory):
+ """Test the SuggestionWidget class."""
+
+ layer = DatabaseFunctionalLayer
+
+ SAFE_OBJECT = Simple('token-1', 'Safe title')
+ UNSAFE_OBJECT = Simple('token-2', '<unsafe> title')
+
+ SAFE_TERM = SimpleTerm(
+ SAFE_OBJECT, SAFE_OBJECT.name, SAFE_OBJECT.displayname)
+ UNSAFE_TERM = SimpleTerm(
+ UNSAFE_OBJECT, UNSAFE_OBJECT.name, UNSAFE_OBJECT.displayname)
+
+ class ExampleSuggestionWidget(SuggestionWidget):
+
+ @staticmethod
+ def _getSuggestions(context):
+ return SimpleVocabulary([TestSuggestionWidget.SAFE_TERM])
+
+ def _autoselectOther(self):
+ on_key_press = "selectWidget('%s', event);" % self._otherId()
+ self.other_selection_widget.onKeyPress = on_key_press
+
+ def setUp(self):
+ super(TestSuggestionWidget, self).setUp()
+ request = LaunchpadTestRequest()
+ vocabulary = SimpleHugeVocabulary(
+ [self.SAFE_TERM, self.UNSAFE_TERM])
+ provideUtility(
+ vocabulary, provides=IVocabularyFactory,
+ name='SimpleHugeVocabulary')
+ field = Choice(
+ __name__='test_field', vocabulary="SimpleHugeVocabulary")
+ field = field.bind(object())
+ self.widget = self.ExampleSuggestionWidget(
+ field, vocabulary, request)
+
+ def test_renderItems(self):
+ # Render all vocabulary and the other option as items.
+ markups = self.widget.renderItems(None)
+ self.assertEqual(2, len(markups))
+ expected = (
+ """<input class="radioType" checked="checked" ...
+ value="token-1" /> <label ...>Safe title</label>""")
+ expected_matcher = DocTestMatches(
+ expected, (doctest.NORMALIZE_WHITESPACE |
+ doctest.REPORT_NDIFF | doctest.ELLIPSIS))
+ self.assertThat(markups[0], expected_matcher)
+ expected = (
+ """<input class="radioType" ...
+ onClick="this.form['field.test_field.test_field'].focus()" ...
+ value="other" /> <label ...>Other:</label>
+ <input type="text" value="" ...
+ onKeyPress="selectWidget('field.test_field.1', event);"
+ .../>...""")
+ expected_matcher = DocTestMatches(
+ expected, (doctest.NORMALIZE_WHITESPACE |
+ doctest.REPORT_NDIFF | doctest.ELLIPSIS))
+ self.assertThat(markups[1], expected_matcher)
+
+
def make_target_branch_widget(branch):
"""Given a branch, return a widget for selecting where to land it."""
choice = Choice(vocabulary='Branch').bind(branch)