From f6fd28dd7df22b55c4e5ca68ff33e196fa9d4766 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 26 Mar 2020 11:44:51 +0100
Subject: [PATCH 2/2] search: search on pages and sub pages (#40224)

---
 combo/apps/search/forms.py  | 20 +++++++++++++++++--
 combo/apps/search/models.py | 27 +++++++++++++++++++------
 combo/apps/search/utils.py  |  7 ++++++-
 tests/test_search.py        | 40 +++++++++++++++++++++++++++++++++++++
 4 files changed, 85 insertions(+), 9 deletions(-)

diff --git a/combo/apps/search/forms.py b/combo/apps/search/forms.py
index 55b4997..eee81cb 100644
--- a/combo/apps/search/forms.py
+++ b/combo/apps/search/forms.py
@@ -15,10 +15,12 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from django import forms
+from django.utils.translation import ugettext_lazy as _
 
+from combo.data.models import Page
 from combo.utils.forms import MultiSortWidget
 
-from . import engines
+from . import engines as search_engines
 from .models import SearchCell
 
 
@@ -27,8 +29,22 @@ class SearchCellForm(forms.ModelForm):
         model = SearchCell
         fields = ('_search_services', 'autofocus')
 
+    def get_engines(self):
+        engines = search_engines.get_engines()
+        if '_text' not in engines:
+            return engines
+
+        text_engine = engines['_text']
+        for i, page in enumerate(Page.objects.filter(snapshot__isnull=True, sub_slug='', root_for_search=True)):
+            engines['_text_page_{}'.format(page.slug)] = {
+                'function': text_engine['function'],
+                'label': _('Page "%(page)s" and sub pages Contents') % {'page': page.title},
+            }
+        return engines
+
     def __init__(self, *args, **kwargs):
         super(SearchCellForm, self).__init__(*args, **kwargs)
-        options = [(x, engines.get_engines()[x]['label']) for x in engines.get_engines().keys()]
+        engines = self.get_engines()
+        options = [(x, engines[x]['label']) for x in engines.keys()]
         self.fields['_search_services'].widget = MultiSortWidget(choices=options,
                                                                  with_checkboxes=True)
diff --git a/combo/apps/search/models.py b/combo/apps/search/models.py
index d8c6a18..229083c 100644
--- a/combo/apps/search/models.py
+++ b/combo/apps/search/models.py
@@ -14,8 +14,6 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import os
-
 from django.contrib.auth.models import Group
 from django.contrib.contenttypes import fields
 from django.contrib.contenttypes.models import ContentType
@@ -37,6 +35,19 @@ from combo.utils import get_templated_url
 from . import engines
 
 
+def get_root_page_and_children(service_slug):
+    if not service_slug.startswith('_text_page_'):
+        return
+
+    page_slug = service_slug.replace('_text_page_', '')
+    try:
+        root_page = Page.objects.get(slug=page_slug, root_for_search=True)
+    except (Page.DoesNotExist, Page.MultipleObjectsReturned):
+        return
+
+    return root_page.get_descendants_and_me()
+
+
 @register_cell_class
 class SearchCell(CellBase):
     template_name = 'combo/search-cell.html'
@@ -69,6 +80,8 @@ class SearchCell(CellBase):
         services = []
         for service_slug in self._search_services.get('data') or []:
             service = engines.get(service_slug)
+            if service_slug.startswith('_text_page_'):
+                service = engines.get('_text')
             if service and (service.get('url') or service.get('function')):
                 service['slug'] = service_slug
                 services.append(service)
@@ -142,10 +155,12 @@ class SearchCell(CellBase):
             return render_response(service)
 
         if service.get('function'):  # internal search engine
-            results = {'data': service['function'](request, query)}
+            pages = get_root_page_and_children(service_slug)
+            results = {'data': service['function'](request, query, pages=pages)}
         else:
-            url = get_templated_url(service['url'],
-                    context={'request': request, 'q': query, 'search_service': service})
+            url = get_templated_url(
+                service['url'],
+                context={'request': request, 'q': query, 'search_service': service})
             url = url % {'q': quote(query.encode('utf-8'))}  # if url contains %(q)s
             if url.startswith('/'):
                 url = request.build_absolute_uri(url)
@@ -186,7 +201,7 @@ class SearchCell(CellBase):
         return render_response(service, results)
 
     def has_text_search_service(self):
-        return '_text' in self._search_services.get('data', [])
+        return any(key.startswith('_text') for key in self._search_services.get('data', []))
 
     def missing_index(self):
         return IndexedCell.objects.all().count() == 0
diff --git a/combo/apps/search/utils.py b/combo/apps/search/utils.py
index b688d59..65dac1f 100644
--- a/combo/apps/search/utils.py
+++ b/combo/apps/search/utils.py
@@ -77,7 +77,9 @@ def index_site():
                 indexed_cell.save()
 
 
-def search_site(request, query):
+def search_site(request, query, pages=None):
+    pages = pages or []
+
     if connection.vendor == 'postgresql':
         config = settings.POSTGRESQL_FTS_SEARCH_CONFIG
         vector = SearchVector('title', config=config, weight='A') + SearchVector('indexed_text', config=config, weight='A')
@@ -94,6 +96,9 @@ def search_site(request, query):
                 Q(restricted_groups__in=request.user.groups.all()))
         qs = qs.exclude(excluded_groups__in=request.user.groups.all())
 
+    if pages:
+        qs = qs.filter(page__in=pages)
+
     hits = []
     seen = {}
     for hit in qs:
diff --git a/tests/test_search.py b/tests/test_search.py
index d65b711..5fd57d9 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -309,6 +309,46 @@ def test_search_api(app):
     assert resp.text.count('<li') == 0
 
 
+def test_search_on_root_page_api(app):
+    page = Page.objects.create(title='example page', slug='example-page')
+    TextCell.objects.create(page=page, text='<p>foobar baz</p>', order=0)
+
+    second_page = Page.objects.create(title='second page', slug='second-page', root_for_search=True)
+    TextCell.objects.create(page=second_page, text='<p>other baz</p>', order=0)
+
+    index_site()
+
+    cell = SearchCell.objects.create(page=page, _search_services={'data': ['_text']}, order=0)
+    resp = app.get('/ajax/search/%s/_text/?q=baz' % cell.pk, status=200)
+    assert resp.text.count('<li') == 2
+
+    cell._search_services = {'data': ['_text_page_second-page']}
+    cell.save()
+    resp = app.get('/ajax/search/%s/_text_page_second-page/?q=baz' % cell.pk, status=200)
+    assert resp.text.count('<li') == 1
+
+    # invalid page (not root for search), search everywhere
+    cell._search_services = {'data': ['_text_page_example-page']}
+    cell.save()
+    resp = app.get('/ajax/search/%s/_text_page_example-page/?q=baz' % cell.pk, status=200)
+    assert resp.text.count('<li') == 2
+
+    # page does not exists, search everywhere
+    cell._search_services = {'data': ['_text_page_foo']}
+    cell.save()
+    resp = app.get('/ajax/search/%s/_text_page_foo/?q=baz' % cell.pk, status=200)
+    assert resp.text.count('<li') == 2
+
+    # slug is not unique, search everywhere
+    page.slug = 'second-page'
+    page.root_for_search = True
+    page.save()
+    cell._search_services = {'data': ['_text_page_second-page']}
+    cell.save()
+    resp = app.get('/ajax/search/%s/_text_page_second-page/?q=baz' % cell.pk, status=200)
+    assert resp.text.count('<li') == 2
+
+
 def test_search_external_links(app):
     page = Page(title='example page', slug='example-page')
     page.save()
-- 
2.20.1

