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
=== modified file 'lib/lp/answers/browser/questiontarget.py'
--- lib/lp/answers/browser/questiontarget.py 2011-04-26 15:44:26 +0000
+++ lib/lp/answers/browser/questiontarget.py 2011-05-04 10:28:05 +0000
@@ -450,7 +450,7 @@
450 "can't call matching_faqs_url when matching_faqs_count == 0")450 "can't call matching_faqs_url when matching_faqs_count == 0")
451 collection = IFAQCollection(self.context)451 collection = IFAQCollection(self.context)
452 return canonical_url(collection) + '/+faqs?' + urlencode({452 return canonical_url(collection) + '/+faqs?' + urlencode({
453 'field.search_text': self.search_text,453 'field.search_text': self.search_text.encode('utf-8'),
454 'field.actions.search': 'Search',454 'field.actions.search': 'Search',
455 })455 })
456456
@@ -464,9 +464,9 @@
464 """464 """
465 self.search_params = dict(self.getDefaultFilter())465 self.search_params = dict(self.getDefaultFilter())
466 self.search_params.update(**data)466 self.search_params.update(**data)
467 if self.search_params.get('search_text', None) is not None:467 search_text = self.search_params.get('search_text', None)
468 self.search_params['search_text'] = (468 if search_text is not None:
469 self.search_params['search_text'].strip())469 self.search_params['search_text'] = search_text.strip()
470470
471 def searchResults(self):471 def searchResults(self):
472 """Return the questions corresponding to the search."""472 """Return the questions corresponding to the search."""
473473
=== modified file 'lib/lp/answers/browser/tests/test_questiontarget.py'
--- lib/lp/answers/browser/tests/test_questiontarget.py 2010-10-26 15:47:24 +0000
+++ lib/lp/answers/browser/tests/test_questiontarget.py 2011-05-04 10:28:05 +0000
@@ -6,16 +6,20 @@
6__metaclass__ = type6__metaclass__ = type
77
8import os8import os
9from urllib import quote
910
10from BeautifulSoup import BeautifulSoup11from BeautifulSoup import BeautifulSoup
11
12from zope.component import getUtility12from zope.component import getUtility
1313
14from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities14from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
15from canonical.testing.layers import DatabaseFunctionalLayer15from canonical.testing.layers import DatabaseFunctionalLayer
16from lp.answers.interfaces.questioncollection import IQuestionSet16from lp.answers.interfaces.questioncollection import IQuestionSet
17from lp.app.enums import ServiceUsage17from lp.app.enums import ServiceUsage
18from lp.testing import login_person, person_logged_in, TestCaseWithFactory18from lp.testing import (
19 login_person,
20 person_logged_in,
21 TestCaseWithFactory,
22 )
19from lp.testing.views import create_initialized_view23from lp.testing.views import create_initialized_view
2024
2125
@@ -23,20 +27,29 @@
2327
24 layer = DatabaseFunctionalLayer28 layer = DatabaseFunctionalLayer
2529
26 def linkPackage(self, product, name):30 def test_matching_faqs_url__handles_non_ascii(self):
27 # A helper to setup a legitimate Packaging link between a product31 product = self.factory.makeProduct()
28 # and an Ubuntu source package.32 # Avoid non-ascii character in unicode literal to not upset
29 hoary = getUtility(ILaunchpadCelebrities).ubuntu['hoary']33 # pocket-lint. Bug #776389.
30 sourcepackagename = self.factory.makeSourcePackageName(name)34 non_ascii_string = u'portugu\xeas'
31 sourcepackage = self.factory.makeSourcePackage(35 with person_logged_in(product.owner):
32 sourcepackagename=sourcepackagename, distroseries=hoary)36 self.factory.makeFAQ(product, non_ascii_string)
33 self.factory.makeSourcePackagePublishingHistory(37 form = {
34 sourcepackagename=sourcepackagename, distroseries=hoary)38 'field.search_text': non_ascii_string,
35 product.development_focus.setPackaging(39 'field.status': 'OPEN',
36 hoary, sourcepackagename, product.owner)40 'field.actions.search': 'Search',
3741 }
3842 view = create_initialized_view(
39class TestSearchQuestionsViewCanConfigureAnswers(TestSearchQuestionsView):43 product, '+questions', form=form, method='GET')
44
45 encoded_string = quote(non_ascii_string.encode('utf-8'))
46 # This must not raise UnicodeEncodeError.
47 self.assertIn(encoded_string, view.matching_faqs_url)
48
49
50class TestSearchQuestionsViewCanConfigureAnswers(TestCaseWithFactory):
51
52 layer = DatabaseFunctionalLayer
4053
41 def test_cannot_configure_answers_product_no_edit_permission(self):54 def test_cannot_configure_answers_product_no_edit_permission(self):
42 product = self.factory.makeProduct()55 product = self.factory.makeProduct()
@@ -75,8 +88,10 @@
75 self.assertEqual(False, view.can_configure_answers)88 self.assertEqual(False, view.can_configure_answers)
7689
7790
78class TestSearchQuestionsViewTemplate(TestSearchQuestionsView):91class TestSearchQuestionsViewTemplate(TestCaseWithFactory):
79 """Test the behaviour of SearchQuestionsView.template"""92 """Test the behavior of SearchQuestionsView.template"""
93
94 layer = DatabaseFunctionalLayer
8095
81 def assertViewTemplate(self, context, file_name):96 def assertViewTemplate(self, context, file_name):
82 view = create_initialized_view(context, '+questions')97 view = create_initialized_view(context, '+questions')
@@ -89,21 +104,21 @@
89104
90 def test_template_product_answers_usage_launchpad(self):105 def test_template_product_answers_usage_launchpad(self):
91 product = self.factory.makeProduct()106 product = self.factory.makeProduct()
92 with person_logged_in(product.owner) as owner:107 with person_logged_in(product.owner):
93 product.answers_usage = ServiceUsage.LAUNCHPAD108 product.answers_usage = ServiceUsage.LAUNCHPAD
94 self.assertViewTemplate(product, 'question-listing.pt')109 self.assertViewTemplate(product, 'question-listing.pt')
95110
96 def test_template_projectgroup_answers_usage_unknown(self):111 def test_template_projectgroup_answers_usage_unknown(self):
97 product = self.factory.makeProduct()112 product = self.factory.makeProduct()
98 project_group = self.factory.makeProject(owner=product.owner)113 project_group = self.factory.makeProject(owner=product.owner)
99 with person_logged_in(product.owner) as owner:114 with person_logged_in(product.owner):
100 product.project = project_group115 product.project = project_group
101 self.assertViewTemplate(project_group, 'unknown-support.pt')116 self.assertViewTemplate(project_group, 'unknown-support.pt')
102117
103 def test_template_projectgroup_answers_usage_launchpad(self):118 def test_template_projectgroup_answers_usage_launchpad(self):
104 product = self.factory.makeProduct()119 product = self.factory.makeProduct()
105 project_group = self.factory.makeProject(owner=product.owner)120 project_group = self.factory.makeProject(owner=product.owner)
106 with person_logged_in(product.owner) as owner:121 with person_logged_in(product.owner):
107 product.project = project_group122 product.project = project_group
108 product.answers_usage = ServiceUsage.LAUNCHPAD123 product.answers_usage = ServiceUsage.LAUNCHPAD
109 self.assertViewTemplate(project_group, 'question-listing.pt')124 self.assertViewTemplate(project_group, 'question-listing.pt')
@@ -114,7 +129,7 @@
114129
115 def test_template_distribution_answers_usage_launchpad(self):130 def test_template_distribution_answers_usage_launchpad(self):
116 distribution = self.factory.makeDistribution()131 distribution = self.factory.makeDistribution()
117 with person_logged_in(distribution.owner) as owner:132 with person_logged_in(distribution.owner):
118 distribution.answers_usage = ServiceUsage.LAUNCHPAD133 distribution.answers_usage = ServiceUsage.LAUNCHPAD
119 self.assertViewTemplate(distribution, 'question-listing.pt')134 self.assertViewTemplate(distribution, 'question-listing.pt')
120135
@@ -124,7 +139,7 @@
124139
125 def test_template_DSP_answers_usage_launchpad(self):140 def test_template_DSP_answers_usage_launchpad(self):
126 dsp = self.factory.makeDistributionSourcePackage()141 dsp = self.factory.makeDistributionSourcePackage()
127 with person_logged_in(dsp.distribution.owner) as owner:142 with person_logged_in(dsp.distribution.owner):
128 dsp.distribution.answers_usage = ServiceUsage.LAUNCHPAD143 dsp.distribution.answers_usage = ServiceUsage.LAUNCHPAD
129 self.assertViewTemplate(dsp, 'question-listing.pt')144 self.assertViewTemplate(dsp, 'question-listing.pt')
130145
@@ -133,8 +148,22 @@
133 self.assertViewTemplate(question_set, 'question-listing.pt')148 self.assertViewTemplate(question_set, 'question-listing.pt')
134149
135150
136class TestSearchQuestionsViewUnknown(TestSearchQuestionsView):151class TestSearchQuestionsViewUnknown(TestCaseWithFactory):
137 """Test the behaviour of SearchQuestionsView unknown support."""152 """Test the behavior of SearchQuestionsView unknown support."""
153
154 layer = DatabaseFunctionalLayer
155
156 def linkPackage(self, product, name):
157 # A helper to setup a legitimate Packaging link between a product
158 # and an Ubuntu source package.
159 hoary = getUtility(ILaunchpadCelebrities).ubuntu['hoary']
160 sourcepackagename = self.factory.makeSourcePackageName(name)
161 self.factory.makeSourcePackage(
162 sourcepackagename=sourcepackagename, distroseries=hoary)
163 self.factory.makeSourcePackagePublishingHistory(
164 sourcepackagename=sourcepackagename, distroseries=hoary)
165 product.development_focus.setPackaging(
166 hoary, sourcepackagename, product.owner)
138167
139 def setUp(self):168 def setUp(self):
140 super(TestSearchQuestionsViewUnknown, self).setUp()169 super(TestSearchQuestionsViewUnknown, self).setUp()