← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~adeuring/launchpad/bug-1020443-model into lp:launchpad

 

Abel Deuring has proposed merging lp:~adeuring/launchpad/bug-1020443-model into lp:launchpad.

Requested reviews:
  Abel Deuring (adeuring): code
Related bugs:
  Bug #1020443 in Launchpad itself: "ProgrammingError: spiexceptions.SyntaxError: syntax error in tsquery"
  https://bugs.launchpad.net/launchpad/+bug/1020443

For more details, see:
https://code.launchpad.net/~adeuring/launchpad/bug-1020443-model/+merge/117076

Don't use the characters "&|!" as operators for full text search queries; avoid usgae of ftq(nl), where nl is the result of a call of nl_phrase_search(). This fixes bad search result ranking and avoids errors when the  branch lp:~adeuring/launchpad/bug-1020443-db-patch will be merged.

See https://code.launchpad.net/~adeuring/launchpad/bug-1020443/+merge/115721 and https://code.launchpad.net/~adeuring/launchpad/bug-1020443-2/+merge/116876 for more details.
-- 
https://code.launchpad.net/~adeuring/launchpad/bug-1020443-model/+merge/117076
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== modified file 'lib/lp/answers/doc/faq-vocabulary.txt'
--- lib/lp/answers/doc/faq-vocabulary.txt	2012-07-27 00:25:42 +0000
+++ lib/lp/answers/doc/faq-vocabulary.txt	2012-07-27 14:59:41 +0000
@@ -82,7 +82,5 @@
     2
     >>> for term in terms:
     ...     print term.title
+    How do I install Extensions?
     How do I troubleshoot problems with extensions/themes?
-    How do I install Extensions?
-
-

=== modified file 'lib/lp/answers/doc/faq.txt'
--- lib/lp/answers/doc/faq.txt	2012-07-27 00:25:42 +0000
+++ lib/lp/answers/doc/faq.txt	2012-07-27 14:59:41 +0000
@@ -232,7 +232,7 @@
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@xxxxxxxxxxxxx')
     >>> for faq in faqset.searchFAQs(
-    ...     search_text='java | flash', owner=foo_bar):
+    ...     search_text='java OR flash', owner=foo_bar):
     ...     print '%s (%s)' % (faq.title, faq.target.displayname)
     How do I install plugins (Shockwave, QuickTime, etc.)? (Mozilla Firefox)
     How can I play MP3/Divx/DVDs/Quicktime/Realmedia files

=== modified file 'lib/lp/answers/model/faq.py'
--- lib/lp/answers/model/faq.py	2012-07-27 00:25:42 +0000
+++ lib/lp/answers/model/faq.py	2012-07-27 14:59:41 +0000
@@ -138,7 +138,7 @@
         return FAQ.select(
             '%s AND FAQ.fti @@ %s' % (target_constraint, quote(fti_search)),
             orderBy=[
-                SQLConstant("-rank(FAQ.fti, ftq(%s))" % quote(fti_search)),
+                SQLConstant("-rank(FAQ.fti, %s::tsquery)" % quote(fti_search)),
                 "-FAQ.date_created"])
 
     @staticmethod

=== modified file 'lib/lp/answers/model/question.py'
--- lib/lp/answers/model/question.py	2012-07-27 00:25:42 +0000
+++ lib/lp/answers/model/question.py	2012-07-27 14:59:41 +0000
@@ -864,6 +864,7 @@
                  product=None, distribution=None, sourcepackagename=None,
                  project=None):
         self.search_text = search_text
+        self.nl_phrase_used = False
 
         if zope_isinstance(status, DBItem):
             self.status = [status]
@@ -944,8 +945,12 @@
         constraints = self.getTargetConstraints()
 
         if self.search_text is not None:
-            constraints.append(
-                'Question.fti @@ ftq(%s)' % quote(self.search_text))
+            if self.nl_phrase_used:
+                constraints.append(
+                    'Question.fti @@ %s' % quote(self.search_text))
+            else:
+                constraints.append(
+                    'Question.fti @@ ftq(%s)' % quote(self.search_text))
 
         if self.status:
             constraints.append('Question.status IN %s' % sqlvalues(
@@ -1009,10 +1014,16 @@
         elif sort is QuestionSort.RELEVANCY:
             if self.search_text:
                 # SQLConstant is a workaround for bug 53455
-                return [SQLConstant(
-                            "-rank(Question.fti, ftq(%s))" % quote(
-                                self.search_text)),
-                        "-Question.datecreated"]
+                if self.nl_phrase_used:
+                    return [SQLConstant(
+                                "-rank(Question.fti, %s::tsquery)" % quote(
+                                    self.search_text)),
+                            "-Question.datecreated"]
+                else:
+                    return [SQLConstant(
+                                "-rank(Question.fti, ftq(%s))" % quote(
+                                    self.search_text)),
+                            "-Question.datecreated"]
             else:
                 return "-Question.datecreated"
         elif sort is QuestionSort.RECENT_OWNER_ACTIVITY:
@@ -1113,6 +1124,7 @@
         # similarity search algorithm.
         self.search_text = nl_phrase_search(
             title, Question, " AND ".join(self.getTargetConstraints()))
+        self.nl_phrase_used = True
 
 
 class QuestionPersonSearch(QuestionSearch):

=== modified file 'lib/lp/answers/stories/this-is-a-faq.txt'
--- lib/lp/answers/stories/this-is-a-faq.txt	2012-07-27 00:25:42 +0000
+++ lib/lp/answers/stories/this-is-a-faq.txt	2012-07-27 14:59:41 +0000
@@ -66,8 +66,8 @@
     ...         print radio, label, link
     >>> printFAQOptions(user_browser.contents)
     (*) No existing FAQs are relevant
+    ( ) 8: How do I install Extensions?
     ( ) 9: How do I troubleshoot problems with extensions/themes?
-    ( ) 8: How do I install Extensions?
 
     >>> print user_browser.getLink('How do I troubleshoot problems').url
     http://answers.launchpad.dev/firefox/+faq/9
@@ -157,8 +157,8 @@
     >>> printFAQOptions(user_browser.contents)
     ( ) No existing FAQs are relevant
     (*) 10: How do I install plugins (Shockwave, QuickTime, etc.)?
+    ( ) 8: How do I install Extensions?
     ( ) 9: How do I troubleshoot problems with extensions/themes?
-    ( ) 8: How do I install Extensions?
 
 He changes the message and click 'Link to FAQ'.
 

=== modified file 'lib/lp/bugs/doc/bugtask-find-similar.txt'
--- lib/lp/bugs/doc/bugtask-find-similar.txt	2012-07-27 00:25:42 +0000
+++ lib/lp/bugs/doc/bugtask-find-similar.txt	2012-07-27 14:59:41 +0000
@@ -142,6 +142,7 @@
     ...     distribution=test_distro)
     >>> for bugtask in similar_bugs:
     ...     print bugtask.bug.title
+    Nothing to do with cheese or sandwiches
     This cheese sandwich should show up
 
     >>> similar_bugs = getUtility(IBugTaskSet).findSimilar(

=== modified file 'lib/lp/bugs/doc/bugtask-search.txt'
--- lib/lp/bugs/doc/bugtask-search.txt	2012-07-27 00:25:42 +0000
+++ lib/lp/bugs/doc/bugtask-search.txt	2012-07-27 14:59:41 +0000
@@ -138,7 +138,7 @@
 
 For example, there are no bugs with the word 'Fnord' in Firefox.
 
-    >>> text_search = BugTaskSearchParams(user=None, fast_searchtext=u'Fnord')
+    >>> text_search = BugTaskSearchParams(user=None, searchtext=u'Fnord')
     >>> found_bugtasks = firefox.searchTasks(text_search)
     >>> found_bugtasks.count()
     0
@@ -156,6 +156,63 @@
     ...     print "#%s" % bugtask.bug.id
     #4
 
+=== BugTaskSearchParams' parameters searchtext and fast_searchtext ===
+
+Normally, the parameter searchtext should be used. The alternative
+parameter fast_searchtext requires a syntactically correct tsquery
+expression containing stemmed words.
+
+A simple phrase can be passed as searchtext, but not as fast_searchtext,
+see below.
+
+    >>> good_search = BugTaskSearchParams(
+    ...     user=None, searchtext=u'happens pretty often')
+    >>> found_bugtasks = firefox.searchTasks(good_search)
+    >>> for bugtask in found_bugtasks:
+    ...     print "#%s" % bugtask.bug.id
+    #4
+
+The unstemmed word "happens" does not yield any results when used
+as fast_textsearch.
+
+    >>> bad_search = BugTaskSearchParams(
+    ...     user=None, fast_searchtext=u'happens')
+    >>> found_bugtasks = firefox.searchTasks(bad_search)
+    >>> print found_bugtasks.count()
+    0
+
+If the stem of "happens" is used, we get results.
+
+    >>> good_search = BugTaskSearchParams(
+    ...     user=None, fast_searchtext=u'happen')
+    >>> found_bugtasks = firefox.searchTasks(good_search)
+    >>> for bugtask in found_bugtasks:
+    ...     print "#%s" % bugtask.bug.id
+    #4
+    #6
+
+Stemmed words may be combined into a valid tsquery expression.
+
+    >>> good_search = BugTaskSearchParams(
+    ...     user=None, fast_searchtext=u'happen&pretti&often')
+    >>> found_bugtasks = firefox.searchTasks(good_search)
+    >>> for bugtask in found_bugtasks:
+    ...     print "#%s" % bugtask.bug.id
+    #4
+
+Passing invalid tsquery expressions as fast_searchtext raises an exception.
+
+    >>> bad_search = BugTaskSearchParams(
+    ...     user=None, fast_searchtext=u'happens pretty often')
+    >>> list(firefox.searchTasks(bad_search))
+    Traceback (most recent call last):
+    ...
+    ProgrammingError: syntax error in tsquery: "happens pretty often"
+    ...
+
+    >>> import transaction
+    >>> transaction.abort()
+
 
 == Searching by bug reporter ==
 
@@ -340,7 +397,6 @@
 bugs are found for him:
 
     >>> from lp.registry.interfaces.person import IPersonSet
-    >>> import transaction
     >>> transaction.abort()
 
     >>> no_priv = getUtility(IPersonSet).getByName('no-priv')

=== modified file 'lib/lp/bugs/model/bugtasksearch.py'
--- lib/lp/bugs/model/bugtasksearch.py	2012-07-27 00:25:42 +0000
+++ lib/lp/bugs/model/bugtasksearch.py	2012-07-27 14:59:41 +0000
@@ -862,18 +862,21 @@
         assert params.searchtext is None, (
             'Cannot use searchtext at the same time as fast_searchtext.')
         searchtext = params.fast_searchtext
+        fti_expression = "?::tsquery"
     else:
         assert params.fast_searchtext is None, (
             'Cannot use fast_searchtext at the same time as searchtext.')
         searchtext = params.searchtext
+        fti_expression = "ftq(?)"
 
     if params.orderby is None:
         # Unordered search results aren't useful, so sort by relevance
         # instead.
         params.orderby = [
-            SQL("-rank(BugTaskFlat.fti, ftq(?))", params=(searchtext,))]
+            SQL("-rank(BugTaskFlat.fti, %s)" % fti_expression,
+                params=(searchtext,))]
 
-    return SQL("BugTaskFlat.fti @@ ftq(?)", params=(searchtext,))
+    return SQL("BugTaskFlat.fti @@ %s" % fti_expression, params=(searchtext,))
 
 
 def _build_status_clause(col, status):

=== modified file 'lib/lp/bugs/model/tests/test_bugtasksearch.py'
--- lib/lp/bugs/model/tests/test_bugtasksearch.py	2012-07-27 00:25:42 +0000
+++ lib/lp/bugs/model/tests/test_bugtasksearch.py	2012-07-27 14:59:41 +0000
@@ -285,9 +285,11 @@
 
     def test_fast_fulltext_search(self):
         # Fast full text searches find text indexed by Bug.fti...
+        # Note that a valid tsquery expression with stemmed words must
+        # be specified.
         self.setUpFullTextSearchTests()
         params = self.getBugTaskSearchParams(
-            user=None, fast_searchtext=u'one title')
+            user=None, fast_searchtext=u'one&titl')
         self.assertSearchFinds(params, self.bugtasks[:1])
 
     def test_tags(self):
@@ -750,7 +752,7 @@
         # Someone without permission to see deactiveated projects does
         # not see bugtasks for deactivated projects.
         bugtask_set = getUtility(IBugTaskSet)
-        param = BugTaskSearchParams(user=None, fast_searchtext=u'Monkeys')
+        param = BugTaskSearchParams(user=None, searchtext=u'Monkeys')
         results = bugtask_set.search(param, _noprejoins=True)
         self.assertEqual([self.active_bugtask], list(results))
 

=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py	2012-07-27 00:25:42 +0000
+++ lib/lp/registry/browser/product.py	2012-07-27 14:59:41 +0000
@@ -205,7 +205,7 @@
     )
 
 
-OR = '|'
+OR = ' OR '
 SPACE = ' '
 
 

=== modified file 'lib/lp/registry/doc/vocabularies.txt'
--- lib/lp/registry/doc/vocabularies.txt	2012-07-27 00:25:42 +0000
+++ lib/lp/registry/doc/vocabularies.txt	2012-07-27 14:59:41 +0000
@@ -684,6 +684,11 @@
     >>> checked_count == len(INACTIVE_ACCOUNT_STATUSES)
     True
 
+It is possible to search for alternative names.
+
+    >>> [person.name for person in vocab.search('matsubara OR salgado')]
+    [u'matsubara', u'salgado']
+
 
 AdminMergeablePerson
 --------------------
@@ -738,6 +743,11 @@
     >>> fooperson in vocab
     False
 
+It is possible to search for alternative names.
+
+    >>> [(p.name) for p in vocab.search('matsubara OR salgado')]
+    [u'matsubara', u'salgado']
+
 
 ValidPersonOrTeam
 .................
@@ -1050,7 +1060,7 @@
     [(u'testing Spanish team', u'Carlos Perell\xf3 Mar\xedn')]
 
     >>> sorted((team.displayname, team.teamowner.displayname)
-    ...        for team in vocab.search('spanish | ubuntu'))
+    ...        for team in vocab.search('spanish OR ubuntu'))
     [(u'Mirror Administrators', u'Mark Shuttleworth'),
      (u'Ubuntu Gnome Team', u'Mark Shuttleworth'),
      (u'Ubuntu Security Team', u'Colin Watson'),

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2012-07-27 00:25:42 +0000
+++ lib/lp/registry/model/person.py	2012-07-27 14:59:41 +0000
@@ -3483,7 +3483,8 @@
             return EmptyResultSet()
 
         orderBy = Person._sortingColumnsForSetOperations
-        text = ensure_unicode(text).lower()
+        text = ensure_unicode(text)
+        lower_case_text = text.lower()
         # Teams may not have email addresses, so we need to either use a LEFT
         # OUTER JOIN or do a UNION between four queries. Using a UNION makes
         # it a lot faster than with a LEFT OUTER JOIN.
@@ -3493,7 +3494,7 @@
             EmailAddress.person == Person.id,
             Person.account == Account.id,
             Not(Account.status.is_in(INACTIVE_ACCOUNT_STATUSES)),
-            EmailAddress.email.lower().startswith(text))
+            EmailAddress.email.lower().startswith(lower_case_text))
 
         store = IStore(Person)
 
@@ -3516,7 +3517,7 @@
 
         results = results.union(store.find(
             Person, person_name_query)).order_by()
-        team_email_query = self._teamEmailQuery(text)
+        team_email_query = self._teamEmailQuery(lower_case_text)
         results = results.union(
             store.find(Person, team_email_query)).order_by()
         team_name_query = self._teamNameQuery(text)
@@ -3530,7 +3531,7 @@
             must_have_email=False, created_after=None, created_before=None):
         """See `IPersonSet`."""
         orderBy = Person._sortingColumnsForSetOperations
-        text = ensure_unicode(text).lower()
+        text = ensure_unicode(text)
         store = IStore(Person)
         base_query = And(
             Person.teamowner == None,
@@ -3570,7 +3571,7 @@
         email_query = And(
             base_query,
             EmailAddress.person == Person.id,
-            EmailAddress.email.lower().startswith(ensure_unicode(text)))
+            EmailAddress.email.lower().startswith(text.lower()))
 
         name_query = And(
             base_query,
@@ -3583,11 +3584,11 @@
     def findTeam(self, text=""):
         """See `IPersonSet`."""
         orderBy = Person._sortingColumnsForSetOperations
-        text = ensure_unicode(text).lower()
+        text = ensure_unicode(text)
         # Teams may not have email addresses, so we need to either use a LEFT
         # OUTER JOIN or do a UNION between two queries. Using a UNION makes
         # it a lot faster than with a LEFT OUTER JOIN.
-        email_query = self._teamEmailQuery(text)
+        email_query = self._teamEmailQuery(text.lower())
         store = IStore(Person)
         email_results = store.find(Person, email_query).order_by()
         name_query = self._teamNameQuery(text)

=== modified file 'lib/lp/registry/tests/test_person_vocabularies.py'
--- lib/lp/registry/tests/test_person_vocabularies.py	2012-07-27 00:25:42 +0000
+++ lib/lp/registry/tests/test_person_vocabularies.py	2012-07-27 14:59:41 +0000
@@ -25,6 +25,7 @@
 from lp.services.webapp.vocabulary import FilteredVocabularyBase
 from lp.testing import (
     login_person,
+    person_logged_in,
     StormStatementRecorder,
     TestCaseWithFactory,
     )
@@ -188,6 +189,22 @@
             name="fredteam", email="fredteam@xxxxxxx")
         self._team_filter_tests([team])
 
+    def test_search_accepts_or_expressions(self):
+        person = self.factory.makePerson(name='baz')
+        team = self.factory.makeTeam(name='blah')
+        result = list(self.searchVocabulary(None, 'baz OR blah'))
+        self.assertEqual([person, team], result)
+        private_team_one = self.factory.makeTeam(
+            name='private-eye', visibility=PersonVisibility.PRIVATE,
+            owner=person)
+        private_team_two = self.factory.makeTeam(
+            name='paranoid', visibility=PersonVisibility.PRIVATE,
+            owner=person)
+        with person_logged_in(person):
+            result = list(
+                self.searchVocabulary(None, 'paranoid OR private-eye'))
+        self.assertEqual([private_team_one, private_team_two], result)
+
 
 class TestValidPersonOrTeamPreloading(VocabularyTestBase,
                                       TestCaseWithFactory):
@@ -373,13 +390,20 @@
 
     def test_unvalidated_emails_ignored(self):
         person = self.factory.makePerson()
-        unvalidated_email = self.factory.makeEmail(
+        self.factory.makeEmail(
             'fnord@xxxxxxxxxxx',
             person,
             email_status=EmailAddressStatus.NEW)
         search = self.searchVocabulary(None, 'fnord@xxxxxxxxxxx')
         self.assertEqual([], [s for s in search])
 
+    def test_search_accepts_or_expressions(self):
+        team_one = self.factory.makeTeam(name='baz')
+        team_two = self.factory.makeTeam(name='blah')
+        result = list(self.searchVocabulary(None, 'baz OR blah'))
+        self.assertEqual([team_one, team_two], result)
+
+
 class TestNewPillarGranteeVocabulary(VocabularyTestBase,
                                         TestCaseWithFactory):
     """Test that the NewPillarGranteeVocabulary behaves as expected."""

=== modified file 'lib/lp/registry/tests/test_personset.py'
--- lib/lp/registry/tests/test_personset.py	2012-07-27 00:25:42 +0000
+++ lib/lp/registry/tests/test_personset.py	2012-07-27 14:59:41 +0000
@@ -117,13 +117,13 @@
 
     def test_getByEmail_ignores_unvalidated_emails(self):
         person = self.factory.makePerson()
-        unvalidated_email = self.factory.makeEmail(
+        self.factory.makeEmail(
             'fnord@xxxxxxxxxxx',
             person,
             email_status=EmailAddressStatus.NEW)
         found = self.person_set.getByEmail('fnord@xxxxxxxxxxx')
         self.assertTrue(found is None)
-        
+
     def test_getPrecachedPersonsFromIDs(self):
         # The getPrecachedPersonsFromIDs() method should only make one
         # query to load all the extraneous data. Accessing the
@@ -183,6 +183,57 @@
             self.person_set.getByOpenIDIdentifier(
                 u'http://not.launchpad.dev/+id/%s' % identifier))
 
+    def test_find__accepts_queries_with_or_operator(self):
+        # PersonSet.find() allows to search for OR combined terms.
+        person_one = self.factory.makePerson(name='baz')
+        person_two = self.factory.makeTeam(name='blah')
+        result = list(self.person_set.find('baz OR blah'))
+        self.assertEqual([person_one, person_two], result)
+
+    def test_findPerson__accepts_queries_with_or_operator(self):
+        # PersonSet.findPerson() allows to search for OR combined terms.
+        person_one = self.factory.makePerson(
+            name='baz', email='one@xxxxxxxxxxx')
+        person_two = self.factory.makePerson(
+            name='blah', email='two@xxxxxxxxxxx')
+        result = list(self.person_set.findPerson('baz OR blah'))
+        self.assertEqual([person_one, person_two], result)
+        # Note that these OR searches do not work for email addresses.
+        result = list(self.person_set.findPerson(
+            'one@xxxxxxxxxxx OR two@xxxxxxxxxxx'))
+        self.assertEqual([], result)
+
+    def test_findPerson__case_insensitive_email_address_search(self):
+        # A search for email addresses is case insensitve.
+        person_one = self.factory.makePerson(
+            name='baz', email='ONE@xxxxxxxxxxx')
+        person_two = self.factory.makePerson(
+            name='blah', email='two@xxxxxxxxxxx')
+        result = list(self.person_set.findPerson('one@xxxxxxxxxxx'))
+        self.assertEqual([person_one], result)
+        result = list(self.person_set.findPerson('TWO@xxxxxxxxxxx'))
+        self.assertEqual([person_two], result)
+
+    def test_findTeam__accepts_queries_with_or_operator(self):
+        # PersonSet.findTeam() allows to search for OR combined terms.
+        team_one = self.factory.makeTeam(name='baz', email='ONE@xxxxxxxxxxx')
+        team_two = self.factory.makeTeam(name='blah', email='TWO@xxxxxxxxxxx')
+        result = list(self.person_set.findTeam('baz OR blah'))
+        self.assertEqual([team_one, team_two], result)
+        # Note that these OR searches do not work for email addresses.
+        result = list(self.person_set.findTeam(
+            'one@xxxxxxxxxxx OR two@xxxxxxxxxxx'))
+        self.assertEqual([], result)
+
+    def test_findTeam__case_insensitive_email_address_search(self):
+        # A search for email addresses is case insensitve.
+        team_one = self.factory.makeTeam(name='baz', email='ONE@xxxxxxxxxxx')
+        team_two = self.factory.makeTeam(name='blah', email='TWO@xxxxxxxxxxx')
+        result = list(self.person_set.findTeam('one@xxxxxxxxxxx'))
+        self.assertEqual([team_one], result)
+        result = list(self.person_set.findTeam('TWO@xxxxxxxxxxx'))
+        self.assertEqual([team_two], result)
+
 
 class TestPersonSetMergeMailingListSubscriptions(TestCaseWithFactory):
 

=== modified file 'lib/lp/registry/tests/test_product_vocabularies.py'
--- lib/lp/registry/tests/test_product_vocabularies.py	2012-07-27 00:25:42 +0000
+++ lib/lp/registry/tests/test_product_vocabularies.py	2012-07-27 14:59:41 +0000
@@ -66,6 +66,16 @@
         self.assertEqual(
             [quux_product, bar_product], list(result))
 
+    def test_search_with_or_expression(self):
+        # Searches for either of two or more names are possible.
+        blah_product = self.factory.makeProduct(
+            name='blah', displayname='Blah', summary='Blah blather')
+        baz_product = self.factory.makeProduct(
+            name='baz', displayname='Baz')
+        result = self.vocabulary.search('blah OR baz')
+        self.assertEqual(
+            [blah_product, baz_product], list(result))
+
     def test_exact_match_is_first(self):
         # When the flag is enabled, an exact name match always wins.
         the_quux_product = self.factory.makeProduct(

=== added file 'lib/lp/registry/tests/test_projectgroup_vocabulary.py'
--- lib/lp/registry/tests/test_projectgroup_vocabulary.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_projectgroup_vocabulary.py	2012-07-27 14:59:41 +0000
@@ -0,0 +1,26 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test the ProjectGroup vocabulary."""
+
+__metaclass__ = type
+
+from lp.registry.vocabularies import ProjectGroupVocabulary
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestProjectGroupVocabulary(TestCaseWithFactory):
+    """Test that the ProjectGroupVocabulary behaves as expected."""
+    layer = DatabaseFunctionalLayer
+
+    def test_search_with_or_expression(self):
+        # Searches for either of two or more names are possible.
+        blah_group = self.factory.makeProject(
+            name='blah', displayname='Blah', summary='Blah blather')
+        baz_group = self.factory.makeProject(
+            name='baz', displayname='Baz')
+        vocabulary = ProjectGroupVocabulary()
+        result = vocabulary.search('blah OR baz')
+        self.assertEqual(
+            [blah_group, baz_group], list(result))

=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py	2012-07-27 03:57:33 +0000
+++ lib/lp/registry/vocabularies.py	2012-07-27 14:59:41 +0000
@@ -304,8 +304,9 @@
         if query is None or an empty string.
         """
         if query:
-            query = ensure_unicode(query).lower()
-            like_query = "'%%' || %s || '%%'" % quote_like(query)
+            query = ensure_unicode(query)
+            like_query = query.lower()
+            like_query = "'%%' || %s || '%%'" % quote_like(like_query)
             fti_query = quote(query)
             sql = "active = 't' AND (name LIKE %s OR fti @@ ftq(%s))" % (
                     like_query, fti_query)
@@ -354,8 +355,9 @@
         if query is None or an empty string.
         """
         if query:
-            query = ensure_unicode(query).lower()
-            like_query = "'%%' || %s || '%%'" % quote_like(query)
+            query = ensure_unicode(query)
+            like_query = query.lower()
+            like_query = "'%%' || %s || '%%'" % quote_like(like_query)
             fti_query = quote(query)
             sql = "active = 't' AND (name LIKE %s OR fti @@ ftq(%s))" % (
                     like_query, fti_query)
@@ -435,7 +437,7 @@
         if not text:
             return self.emptySelectResults()
 
-        return self._select(ensure_unicode(text).lower())
+        return self._select(ensure_unicode(text))
 
 
 class PersonAccountToMergeVocabulary(
@@ -469,7 +471,7 @@
         if not text:
             return self.emptySelectResults()
 
-        text = ensure_unicode(text).lower()
+        text = ensure_unicode(text)
         return self._select(text)
 
 
@@ -649,14 +651,15 @@
                 FROM (
                     SELECT Person.id,
                     (case
-                        when person.name=? then 100
-                        when person.name like ? || '%%' then 0.6
-                        when lower(person.displayname) like ? || '%%' then 0.5
+                        when person.name=lower(?) then 100
+                        when person.name like lower(?) || '%%' then 0.6
+                        when lower(person.displayname) like lower(?)
+                            || '%%' then 0.5
                         else rank(fti, ftq(?))
                     end) as rank
                     FROM Person
-                    WHERE Person.name LIKE ? || '%%'
-                    or lower(Person.displayname) LIKE ? || '%%'
+                    WHERE Person.name LIKE lower(?) || '%%'
+                    or lower(Person.displayname) LIKE lower(?) || '%%'
                     or Person.fti @@ ftq(?)
                     UNION ALL
                     SELECT Person.id, 0.8 AS rank
@@ -667,7 +670,7 @@
                     SELECT Person.id, 0.4 AS rank
                     FROM Person, EmailAddress
                     WHERE Person.id = EmailAddress.person
-                        AND LOWER(EmailAddress.email) LIKE ? || '%%'
+                        AND LOWER(EmailAddress.email) LIKE lower(?) || '%%'
                         AND status IN (?, ?)
                 ) AS person_match
                 GROUP BY id, is_private_team
@@ -680,9 +683,10 @@
                 private_tables = [Person] + private_tables
                 private_ranking_sql = SQL("""
                     (case
-                        when person.name=? then 100
-                        when person.name like ? || '%%' then 0.6
-                        when lower(person.displayname) like ? || '%%' then 0.5
+                        when person.name=lower(?) then 100
+                        when person.name like lower(?) || '%%' then 0.6
+                        when lower(person.displayname) like lower(?)
+                            || '%%' then 0.5
                         else rank(fti, ftq(?))
                     end) as rank
                 """, (text, text, text, text))
@@ -696,8 +700,8 @@
                                 SQL("true as is_private_team")),
                     where=And(
                         SQL("""
-                            Person.name LIKE ? || '%%'
-                            OR lower(Person.displayname) LIKE ? || '%%'
+                            Person.name LIKE lower(?) || '%%'
+                            OR lower(Person.displayname) LIKE lower(?) || '%%'
                             OR Person.fti @@ ftq(?)
                             """, [text, text, text]),
                         private_query))
@@ -792,7 +796,7 @@
             else:
                 return self.emptySelectResults()
 
-        text = ensure_unicode(text).lower()
+        text = ensure_unicode(text)
         return self._doSearch(text=text, vocab_filter=vocab_filter)
 
     def searchForTerms(self, query=None, vocab_filter=None):
@@ -838,8 +842,8 @@
             result = self.store.using(*tables).find(Person, query)
         else:
             name_match_query = SQL("""
-                Person.name LIKE ? || '%%'
-                OR lower(Person.displayname) LIKE ? || '%%'
+                Person.name LIKE lower(?) || '%%'
+                OR lower(Person.displayname) LIKE lower(?) || '%%'
                 OR Person.fti @@ ftq(?)
                 """, [text, text, text]),
 


Follow ups