Merge lp:~henninge/launchpad/devel-644872-unicode-error-in-search-text into lp:launchpad

Proposed by Henning Eggers
Status: Merged
Approved by: Henning Eggers
Approved revision: no longer in the source branch.
Merged at revision: 12966
Proposed branch: lp:~henninge/launchpad/devel-644872-unicode-error-in-search-text
Merge into: lp:launchpad
Diff against target: 177 lines (+58/-29)
2 files modified
lib/lp/answers/browser/questiontarget.py (+4/-4)
lib/lp/answers/browser/tests/test_questiontarget.py (+54/-25)
To merge this branch: bzr merge lp:~henninge/launchpad/devel-644872-unicode-error-in-search-text
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+59783@code.launchpad.net

Commit message

[r=gmb][bug=644872] Handle non-ascii strings in links to FAQs on an answers search page.

Description of the change

= Summary =

Searching for a non-ascii term caused a problem when there was lso an
FAQ for that term. The view method that created the link could not
handle non-ascii code.

== Proposed fix ==

Encode the string as utf-8. If the string is not unicode, this is a nop.

== Tests ==

I cleaned up the test module for questiontarget and added a test for
this bug. It really just needs to make sure that no exception gets
raised.

bin/test -vvcm lp.answers.browser.tests.test_questiontarget

== Demo and Q/A ==

Search for "português" on http://answers.qastaging.launchpad.net/ubuntu
It should not raise an exception anymore.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/answers/browser/tests/test_questiontarget.py
  lib/lp/answers/browser/questiontarget.py

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) :
review: Approve (code)
Revision history for this message
Robert Collins (lifeless) wrote :

This is potentially buggy:
>>> print u'foo\xce'.encode('utf8').encode('utf8')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position
3: ordinal not in range(128)

Do we know that the thing being encoded is always unicode? If so, the
patch is fine. If it /might be/ bytes already re-encoding would go
boom.

Revision history for this message
Henning Eggers (henninge) wrote :

I had thought about that but did not reach that conclusion (i.e. that it might already be utf-8). I suggest it should only re-encode unicode strings, or should it do some "trying out"?

Somehow I wonder, though, if this might or should not be solved globally. Shouldn't the widget code take care of these issues and always present me with, say, a unicode object?

Revision history for this message
Robert Collins (lifeless) wrote :

On Wed, May 4, 2011 at 8:50 PM, Henning Eggers
<email address hidden> wrote:
> I had thought about that but did not reach that conclusion (i.e. that it might already be utf-8). I suggest it should only re-encode unicode strings, or should it do some "trying out"?
>
> Somehow I wonder, though, if this might or should not be solved globally. Shouldn't the widget code take care of these issues and always present me with, say, a unicode object?

Guaranteeing that the strings inside the webapp code are unicode
objects would be pretty sane(1)(2). Alternatively programming
defensively and doing 'if type(thing) is unicode: thing =
thing.encode('utf8')' would also be sane.

1) but possibly hard: form data might be bogus or whatever. We can
probably afford to just raise UFD on those.
2) it may already be guaranteed; I'm not sure.

-Rob

Revision history for this message
Henning Eggers (henninge) wrote :

Am 04.05.2011 11:41, schrieb Robert Collins:
> Alternatively programming
> defensively and doing 'if type(thing) is unicode: thing =
> thing.encode('utf8')' would also be sane.

Yeah, I'll add that.

> 2) it may already be guaranteed; I'm not sure.

That's actually what I meant.

Henning

Revision history for this message
Henning Eggers (henninge) wrote :

While writing a test to try out passing in utf-8 strings, it became clear that zope.form uses unicode internally and does not like utf-8 being passed in. So, I think it is safe to assume that search_text is unicode. No change needed.

Tail of traceback:

  File "/home/henning/canonical/lp-sourcedeps/eggs/zope.app.form-3.8.1-py2.6.egg/zope/app/form/browser/widget.py", line 317, in getInputValue
    value = self._toFieldValue(self._getFormInput())
  File "/home/henning/canonical/lp-sourcedeps/eggs/zope.app.form-3.8.1-py2.6.egg/zope/app/form/browser/textwidgets.py", line 139, in _toFieldValue
    if self.convert_missing_value and input == self._missing:
UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/answers/browser/questiontarget.py'
2--- lib/lp/answers/browser/questiontarget.py 2011-04-26 15:44:26 +0000
3+++ lib/lp/answers/browser/questiontarget.py 2011-05-04 10:28:05 +0000
4@@ -450,7 +450,7 @@
5 "can't call matching_faqs_url when matching_faqs_count == 0")
6 collection = IFAQCollection(self.context)
7 return canonical_url(collection) + '/+faqs?' + urlencode({
8- 'field.search_text': self.search_text,
9+ 'field.search_text': self.search_text.encode('utf-8'),
10 'field.actions.search': 'Search',
11 })
12
13@@ -464,9 +464,9 @@
14 """
15 self.search_params = dict(self.getDefaultFilter())
16 self.search_params.update(**data)
17- if self.search_params.get('search_text', None) is not None:
18- self.search_params['search_text'] = (
19- self.search_params['search_text'].strip())
20+ search_text = self.search_params.get('search_text', None)
21+ if search_text is not None:
22+ self.search_params['search_text'] = search_text.strip()
23
24 def searchResults(self):
25 """Return the questions corresponding to the search."""
26
27=== modified file 'lib/lp/answers/browser/tests/test_questiontarget.py'
28--- lib/lp/answers/browser/tests/test_questiontarget.py 2010-10-26 15:47:24 +0000
29+++ lib/lp/answers/browser/tests/test_questiontarget.py 2011-05-04 10:28:05 +0000
30@@ -6,16 +6,20 @@
31 __metaclass__ = type
32
33 import os
34+from urllib import quote
35
36 from BeautifulSoup import BeautifulSoup
37-
38 from zope.component import getUtility
39
40 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
41 from canonical.testing.layers import DatabaseFunctionalLayer
42 from lp.answers.interfaces.questioncollection import IQuestionSet
43 from lp.app.enums import ServiceUsage
44-from lp.testing import login_person, person_logged_in, TestCaseWithFactory
45+from lp.testing import (
46+ login_person,
47+ person_logged_in,
48+ TestCaseWithFactory,
49+ )
50 from lp.testing.views import create_initialized_view
51
52
53@@ -23,20 +27,29 @@
54
55 layer = DatabaseFunctionalLayer
56
57- def linkPackage(self, product, name):
58- # A helper to setup a legitimate Packaging link between a product
59- # and an Ubuntu source package.
60- hoary = getUtility(ILaunchpadCelebrities).ubuntu['hoary']
61- sourcepackagename = self.factory.makeSourcePackageName(name)
62- sourcepackage = self.factory.makeSourcePackage(
63- sourcepackagename=sourcepackagename, distroseries=hoary)
64- self.factory.makeSourcePackagePublishingHistory(
65- sourcepackagename=sourcepackagename, distroseries=hoary)
66- product.development_focus.setPackaging(
67- hoary, sourcepackagename, product.owner)
68-
69-
70-class TestSearchQuestionsViewCanConfigureAnswers(TestSearchQuestionsView):
71+ def test_matching_faqs_url__handles_non_ascii(self):
72+ product = self.factory.makeProduct()
73+ # Avoid non-ascii character in unicode literal to not upset
74+ # pocket-lint. Bug #776389.
75+ non_ascii_string = u'portugu\xeas'
76+ with person_logged_in(product.owner):
77+ self.factory.makeFAQ(product, non_ascii_string)
78+ form = {
79+ 'field.search_text': non_ascii_string,
80+ 'field.status': 'OPEN',
81+ 'field.actions.search': 'Search',
82+ }
83+ view = create_initialized_view(
84+ product, '+questions', form=form, method='GET')
85+
86+ encoded_string = quote(non_ascii_string.encode('utf-8'))
87+ # This must not raise UnicodeEncodeError.
88+ self.assertIn(encoded_string, view.matching_faqs_url)
89+
90+
91+class TestSearchQuestionsViewCanConfigureAnswers(TestCaseWithFactory):
92+
93+ layer = DatabaseFunctionalLayer
94
95 def test_cannot_configure_answers_product_no_edit_permission(self):
96 product = self.factory.makeProduct()
97@@ -75,8 +88,10 @@
98 self.assertEqual(False, view.can_configure_answers)
99
100
101-class TestSearchQuestionsViewTemplate(TestSearchQuestionsView):
102- """Test the behaviour of SearchQuestionsView.template"""
103+class TestSearchQuestionsViewTemplate(TestCaseWithFactory):
104+ """Test the behavior of SearchQuestionsView.template"""
105+
106+ layer = DatabaseFunctionalLayer
107
108 def assertViewTemplate(self, context, file_name):
109 view = create_initialized_view(context, '+questions')
110@@ -89,21 +104,21 @@
111
112 def test_template_product_answers_usage_launchpad(self):
113 product = self.factory.makeProduct()
114- with person_logged_in(product.owner) as owner:
115+ with person_logged_in(product.owner):
116 product.answers_usage = ServiceUsage.LAUNCHPAD
117 self.assertViewTemplate(product, 'question-listing.pt')
118
119 def test_template_projectgroup_answers_usage_unknown(self):
120 product = self.factory.makeProduct()
121 project_group = self.factory.makeProject(owner=product.owner)
122- with person_logged_in(product.owner) as owner:
123+ with person_logged_in(product.owner):
124 product.project = project_group
125 self.assertViewTemplate(project_group, 'unknown-support.pt')
126
127 def test_template_projectgroup_answers_usage_launchpad(self):
128 product = self.factory.makeProduct()
129 project_group = self.factory.makeProject(owner=product.owner)
130- with person_logged_in(product.owner) as owner:
131+ with person_logged_in(product.owner):
132 product.project = project_group
133 product.answers_usage = ServiceUsage.LAUNCHPAD
134 self.assertViewTemplate(project_group, 'question-listing.pt')
135@@ -114,7 +129,7 @@
136
137 def test_template_distribution_answers_usage_launchpad(self):
138 distribution = self.factory.makeDistribution()
139- with person_logged_in(distribution.owner) as owner:
140+ with person_logged_in(distribution.owner):
141 distribution.answers_usage = ServiceUsage.LAUNCHPAD
142 self.assertViewTemplate(distribution, 'question-listing.pt')
143
144@@ -124,7 +139,7 @@
145
146 def test_template_DSP_answers_usage_launchpad(self):
147 dsp = self.factory.makeDistributionSourcePackage()
148- with person_logged_in(dsp.distribution.owner) as owner:
149+ with person_logged_in(dsp.distribution.owner):
150 dsp.distribution.answers_usage = ServiceUsage.LAUNCHPAD
151 self.assertViewTemplate(dsp, 'question-listing.pt')
152
153@@ -133,8 +148,22 @@
154 self.assertViewTemplate(question_set, 'question-listing.pt')
155
156
157-class TestSearchQuestionsViewUnknown(TestSearchQuestionsView):
158- """Test the behaviour of SearchQuestionsView unknown support."""
159+class TestSearchQuestionsViewUnknown(TestCaseWithFactory):
160+ """Test the behavior of SearchQuestionsView unknown support."""
161+
162+ layer = DatabaseFunctionalLayer
163+
164+ def linkPackage(self, product, name):
165+ # A helper to setup a legitimate Packaging link between a product
166+ # and an Ubuntu source package.
167+ hoary = getUtility(ILaunchpadCelebrities).ubuntu['hoary']
168+ sourcepackagename = self.factory.makeSourcePackageName(name)
169+ self.factory.makeSourcePackage(
170+ sourcepackagename=sourcepackagename, distroseries=hoary)
171+ self.factory.makeSourcePackagePublishingHistory(
172+ sourcepackagename=sourcepackagename, distroseries=hoary)
173+ product.development_focus.setPackaging(
174+ hoary, sourcepackagename, product.owner)
175
176 def setUp(self):
177 super(TestSearchQuestionsViewUnknown, self).setUp()