diff --git a/combo/apps/search/__init__.py b/combo/apps/search/__init__.py index 2a641214..8cffe199 100644 --- a/combo/apps/search/__init__.py +++ b/combo/apps/search/__init__.py @@ -15,6 +15,7 @@ # along with this program. If not, see . import django.apps +from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from .engines import engines @@ -28,4 +29,22 @@ class AppConfig(django.apps.AppConfig): from . import urls return urls.urlpatterns + def hourly(self): + from .utils import index_site + index_site() + + def ready(self): + # register built-in search engine for page contents + engines.register(self.get_search_engines) + + def get_search_engines(self): + from .utils import search_site + return { + '_text': { + 'function': search_site, + 'label': _('Page Contents'), + } + } + + default_app_config = 'combo.apps.search.AppConfig' diff --git a/combo/apps/search/management/__init__.py b/combo/apps/search/management/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/combo/apps/search/management/commands/__init__.py b/combo/apps/search/management/commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/combo/apps/search/management/commands/update_index.py b/combo/apps/search/management/commands/update_index.py deleted file mode 100644 index 37eb6ffa..00000000 --- a/combo/apps/search/management/commands/update_index.py +++ /dev/null @@ -1,78 +0,0 @@ -# combo - content management system -# Copyright (C) 2017 Entr'ouvert -# -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU Affero General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from django.utils.timezone import now - -from haystack.management.commands.update_index import Command as UpdateIndexCommand - -from combo.data.models import Page, ExternalLinkSearchItem -from combo.apps.search.models import SearchCell - - -class Command(UpdateIndexCommand): - - def add_arguments(self, parser): - super(Command, self).add_arguments(parser) - parser.add_argument( - '--skip-external-links-collection', action='store_true', default=False, - dest='skip_external_links_collection') - - def handle(self, **options): - if not any(SearchCell.get_cells_by_search_service('_text')): - # do not index site if there's no matching search cell - return - if not options.get('skip_external_links_collection', False): - self.collect_external_links(options) - return super(Command, self).handle(**options) - - def collect_external_links(self, options): - start_time = now() - - if options.get('remove'): - ExternalLinkSearchItem.objects.all().delete() - - # assemble external links data - links = {} - for page in Page.objects.filter(sub_slug=''): - if not page.is_visible(user=None): - continue - for cell in page.get_cells(): - if not cell.is_visible(user=None): - continue - for link_data in cell.get_external_links_data(): - if not link_data['url'] in links: - # create an entry for that link. - links[link_data['url']] = {} - links[link_data['url']]['title'] = link_data['title'] - links[link_data['url']]['all_texts'] = [] - else: - # if that link already exists, just keep the title as - # text. - links[link_data['url']]['all_texts'].append(link_data['title']) - # additional texts will be assembled and indexed - links[link_data['url']]['all_texts'].append(link_data.get('text') or '') - - # save data as ExternalLinkSearchItem objects - for link_url, link_data in links.items(): - link_object, created = ExternalLinkSearchItem.objects.get_or_create( - url=link_url, - defaults={'title': link_data['title']}) - link_object.title = link_data['title'] - link_object.text = '\n'.join(link_data['all_texts']) - link_object.save() - - # remove obsolete objects - ExternalLinkSearchItem.objects.filter(last_update_timestamp__lt=start_time).delete() diff --git a/combo/apps/search/migrations/0006_indexedcell.py b/combo/apps/search/migrations/0006_indexedcell.py new file mode 100644 index 00000000..a9b2d86a --- /dev/null +++ b/combo/apps/search/migrations/0006_indexedcell.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.17 on 2020-01-20 15:30 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('data', '0043_delete_externallinksearchitem'), + ('auth', '0008_alter_user_username_max_length'), + ('contenttypes', '0002_remove_content_type_name'), + ('search', '0005_searchcell_autofocus'), + ] + + operations = [ + migrations.CreateModel( + name='IndexedCell', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cell_pk', models.PositiveIntegerField(null=True)), + ('url', models.CharField(blank=True, max_length=500, null=True)), + ('title', models.CharField(blank=True, max_length=500, null=True)), + ('indexed_text', models.TextField(blank=True, null=True)), + ('public_access', models.BooleanField(default=False)), + ('last_update_timestamp', models.DateTimeField(auto_now=True)), + ('cell_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('excluded_groups', models.ManyToManyField(blank=True, related_name='_indexedcell_excluded_groups_+', to='auth.Group')), + ('page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='data.Page')), + ('restricted_groups', models.ManyToManyField(blank=True, related_name='_indexedcell_restricted_groups_+', to='auth.Group')), + ], + ), + ] diff --git a/combo/apps/search/models.py b/combo/apps/search/models.py index 5dcd794e..d8c6a181 100644 --- a/combo/apps/search/models.py +++ b/combo/apps/search/models.py @@ -16,21 +16,21 @@ import os -from django.conf import settings +from django.contrib.auth.models import Group +from django.contrib.contenttypes import fields +from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.translation import ugettext_lazy as _ from django import template from django.http import HttpResponse from django.core.exceptions import PermissionDenied -from django.core.urlresolvers import reverse from django.utils.http import quote from django.template import RequestContext, Template from jsonfield import JSONField -from haystack import connections from combo.utils import requests -from combo.data.models import CellBase +from combo.data.models import CellBase, Page from combo.data.library import register_cell_class from combo.utils import get_templated_url @@ -69,7 +69,7 @@ class SearchCell(CellBase): services = [] for service_slug in self._search_services.get('data') or []: service = engines.get(service_slug) - if service and service.get('url'): + if service and (service.get('url') or service.get('function')): service['slug'] = service_slug services.append(service) return services @@ -141,30 +141,33 @@ class SearchCell(CellBase): if not query: return render_response(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) + if service.get('function'): # internal search engine + results = {'data': service['function'](request, query)} + else: + 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) - if not url: - return render_response(service) + if not url: + return render_response(service) - kwargs = {} - kwargs['cache_duration'] = service.get('cache_duration', 0) - kwargs['remote_service'] = 'auto' if service.get('signature') else None - # don't automatically add user info to query string, if required it can - # be set explicitely in the URL template in the engine definition (via - # {{user_nameid}} or {{user_email}}). - kwargs['without_user'] = True - # don't send error traces on HTTP errors - kwargs['log_errors'] = 'warn' + kwargs = {} + kwargs['cache_duration'] = service.get('cache_duration', 0) + kwargs['remote_service'] = 'auto' if service.get('signature') else None + # don't automatically add user info to query string, if required it can + # be set explicitely in the URL template in the engine definition (via + # {{user_nameid}} or {{user_email}}). + kwargs['without_user'] = True + # don't send error traces on HTTP errors + kwargs['log_errors'] = 'warn' - response = requests.get(url, **kwargs) - try: - results = response.json() - except ValueError: - return render_response(service) + response = requests.get(url, **kwargs) + try: + results = response.json() + except ValueError: + return render_response(service) if service.get('data_key'): results['data'] = results.get(service['data_key']) or [] @@ -179,10 +182,25 @@ class SearchCell(CellBase): for hit in results.get('data') or []: for k, v in hit_templates.items(): hit[k] = v.render(RequestContext(request, hit)) + return render_response(service, results) def has_text_search_service(self): return '_text' in self._search_services.get('data', []) def missing_index(self): - return not os.path.exists(connections['default'].get_backend().path) + return IndexedCell.objects.all().count() == 0 + + +class IndexedCell(models.Model): + cell_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + cell_pk = models.PositiveIntegerField(null=True) + cell = fields.GenericForeignKey('cell_type', 'cell_pk') + page = models.ForeignKey(Page, on_delete=models.CASCADE, blank=True, null=True) + url = models.CharField(max_length=500, blank=True, null=True) + title = models.CharField(max_length=500, blank=True, null=True) + indexed_text = models.TextField(blank=True, null=True) + public_access = models.BooleanField(default=False) + restricted_groups = models.ManyToManyField(Group, blank=True, related_name='+') + excluded_groups = models.ManyToManyField(Group, blank=True, related_name='+') + last_update_timestamp = models.DateTimeField(auto_now=True) diff --git a/combo/apps/search/utils.py b/combo/apps/search/utils.py new file mode 100644 index 00000000..9ffe5478 --- /dev/null +++ b/combo/apps/search/utils.py @@ -0,0 +1,111 @@ +# combo - content management system +# Copyright (C) 2014-2020 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector +from combo.data.models import CellBase +from django.db import connection +from django.db.models import Q +from django.db.transaction import atomic + +from .models import IndexedCell + + +def set_cell_access(indexed_cell, cell): + indexed_cell.public_access = bool(cell.page.public and cell.public) + indexed_cell.excluded_groups.clear() + indexed_cell.restricted_groups.clear() + if not indexed_cell.public_access: + indexed_cell.restricted_groups.set(cell.groups.all()) + if cell.restricted_to_unlogged: + indexed_cell.excluded_groups.set(cell.page.groups.all()) + else: + for group in cell.page.groups.all(): + indexed_cell.restricted_groups.add(group) + indexed_cell.save() + + +@atomic +def index_site(): + IndexedCell.objects.all().delete() + external_urls = {} + for klass in CellBase.get_cell_classes(): + for cell in klass.objects.filter(page__snapshot__isnull=True).exclude(placeholder__startswith='_'): + cell_type = ContentType.objects.get_for_model(cell) + indexed_cell = IndexedCell(cell_type=cell_type, cell_pk=cell.id) + try: + indexed_cell.indexed_text = cell.render_for_search() + except Exception: # ignore rendering error + continue + if indexed_cell.indexed_text: + indexed_cell.page_id = cell.page_id + indexed_cell.url = cell.page.get_online_url() + indexed_cell.title = cell.page.title + indexed_cell.save() + set_cell_access(indexed_cell, cell) + + for link_data in cell.get_external_links_data(): + # index external links + indexed_cell = external_urls.get(indexed_cell.url) + if indexed_cell is None: + # create an entry for that link. + indexed_cell = IndexedCell(cell_type=cell_type, cell_pk=cell.id) + indexed_cell.save() + set_cell_access(indexed_cell, cell) + indexed_cell.url = link_data['url'] + indexed_cell.title = link_data['title'] + indexed_cell.indexed_text = link_data.get('text') or '' + external_urls[indexed_cell.url] = indexed_cell + else: + # if that link already exists, add detailed texts + indexed_cell.indexed_text += ' ' + link_data['title'] + indexed_cell.indexed_text += ' ' + link_data.get('text') or '' + indexed_cell.save() + + +def search_site(request, query): + if connection.vendor == 'postgresql': + config = settings.POSTGRESQL_FTS_SEARCH_CONFIG + vector = SearchVector('title', config=config, weight='A') + SearchVector('indexed_text', config=config, weight='A') + query = SearchQuery(query) + qs = IndexedCell.objects.annotate(rank=SearchRank(vector, query)).filter(rank__gte=0.3).order_by('-rank') + else: + qs = IndexedCell.objects.filter( + Q(indexed_text__icontains=query) | Q(title__icontains=query)) + if request.user.is_anonymous: + qs = qs.exclude(public_access=False) + else: + qs = qs.filter( + Q(restricted_groups=None) | + Q(restricted_groups__in=request.user.groups.all())) + qs = qs.exclude(excluded_groups__in=request.user.groups.all()) + + hits = [] + seen = {} + for hit in qs: + if hit.url in seen: + continue + hits.append({ + 'text': hit.title, + 'rank': getattr(hit, 'rank', None), + 'url': hit.url, + }) + seen[hit.url] = True + if len(hits) == 10: + break + + return hits diff --git a/combo/data/apps.py b/combo/data/apps.py index be1e3a0e..673636d9 100644 --- a/combo/data/apps.py +++ b/combo/data/apps.py @@ -15,23 +15,8 @@ # along with this program. If not, see . from django.apps import AppConfig -from django.core.urlresolvers import reverse -from django.utils.translation import ugettext_lazy as _ class DataConfig(AppConfig): name = 'combo.data' verbose_name = 'data' - - def ready(self): - # register built-in search engine for page contents - from combo.apps.search import engines - engines.register(self.get_search_engines) - - def get_search_engines(self): - return { - '_text': { - 'url': reverse('api-search') + '?q=%(q)s', - 'label': _('Page Contents'), - } - } diff --git a/combo/data/migrations/0043_delete_externallinksearchitem.py b/combo/data/migrations/0043_delete_externallinksearchitem.py new file mode 100644 index 00000000..2f6c7718 --- /dev/null +++ b/combo/data/migrations/0043_delete_externallinksearchitem.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.17 on 2020-01-20 15:30 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('data', '0042_page_creation_timestamp'), + ] + + operations = [ + migrations.DeleteModel( + name='ExternalLinkSearchItem', + ), + ] diff --git a/combo/data/models.py b/combo/data/models.py index 59078b80..230a51fc 100644 --- a/combo/data/models.py +++ b/combo/data/models.py @@ -755,10 +755,6 @@ class CellBase(six.with_metaclass(CellMeta, models.Model)): return '' if self.user_dependant: return '' - if not self.page.is_visible(user=None): - return '' - if not self.is_visible(user=None): - return '' request = RequestFactory().get(self.page.get_online_url()) request.user = None # compat context = { @@ -1474,18 +1470,6 @@ class ConfigJsonCell(JsonCellBase): return context -class ExternalLinkSearchItem(models.Model): - # Link to an external site. - # - # Those are automatically collected during by the "update_index" command, - # that calls get_external_links_data from all available cells, to be used - # by the general search engine. - title = models.CharField(_('Title'), max_length=150) - text = models.TextField(blank=True) - url = models.CharField(_('URL'), max_length=200, blank=True) - last_update_timestamp = models.DateTimeField(auto_now=True) - - @receiver(pre_save, sender=Page) def create_redirects(sender, instance, raw, **kwargs): if raw or not instance.id or instance.snapshot_id: diff --git a/combo/data/search_indexes.py b/combo/data/search_indexes.py deleted file mode 100644 index 6b58a08d..00000000 --- a/combo/data/search_indexes.py +++ /dev/null @@ -1,46 +0,0 @@ -# combo - content management system -# Copyright (C) 2014-2017 Entr'ouvert -# -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU Affero General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from haystack import indexes -from haystack.exceptions import SkipDocument - -from .models import Page, CellBase, ExternalLinkSearchItem - -class PageIndex(indexes.SearchIndex, indexes.Indexable): - title = indexes.CharField(model_attr='title', boost=1.5) - text = indexes.CharField(document=True, use_template=True, - template_name='combo/search/page.txt') - url = indexes.CharField(indexed=False) - - def get_model(self): - return Page - - def prepare_url(self, obj): - return obj.get_online_url() - - def prepare(self, obj): - if not obj.is_visible(user=None): - raise SkipDocument() - return super(PageIndex, self).prepare(obj) - - -class ExternalLinkSearchIndex(indexes.SearchIndex, indexes.Indexable): - title = indexes.CharField(model_attr='title', boost=1.5) - text = indexes.CharField(model_attr='text', document=True) - url = indexes.CharField(model_attr='url', indexed=False) - - def get_model(self): - return ExternalLinkSearchItem diff --git a/combo/data/templates/combo/search/page.txt b/combo/data/templates/combo/search/page.txt deleted file mode 100644 index 4e21afaa..00000000 --- a/combo/data/templates/combo/search/page.txt +++ /dev/null @@ -1,7 +0,0 @@ -{% autoescape off %} -{% for cell in object.get_cells %} - {% if cell.placeholder|first != '_' %} {# ignore technical placeholders #} - {{ cell.render_for_search }} - {% endif %} -{% endfor %} -{% endautoescape %} diff --git a/combo/public/urls.py b/combo/public/urls.py index abd6aaee..933d72c5 100644 --- a/combo/public/urls.py +++ b/combo/public/urls.py @@ -21,7 +21,6 @@ from . import views urlpatterns = [ url(r'^api/menu-badges/$', views.menu_badges), - url(r'^api/search/$', views.api_search, name='api-search'), url(r'^ajax/cell/(?P\w+)/(?P[\w_-]+)/$', views.ajax_page_cell, name='combo-public-ajax-page-cell'), url(r'^snapshot/(?P\w+)/$', manager_required(views.snapshot), name='combo-snapshot-view'), diff --git a/combo/public/views.py b/combo/public/views.py index 4f1c518a..aeca4439 100644 --- a/combo/public/views.py +++ b/combo/public/views.py @@ -40,9 +40,6 @@ from django.views.decorators.csrf import csrf_exempt from django.utils.translation import ugettext as _ from django.forms.widgets import Media -from haystack.inputs import AutoQuery -from haystack.query import SearchQuerySet, SQ - if 'mellon' in settings.INSTALLED_APPS: from mellon.utils import get_idps else: @@ -577,31 +574,6 @@ def menu_badges(request): menu_badges.mellon_no_passive = True -def api_search(request): - for cell in SearchCell.get_cells_by_search_service('_text'): - if not cell.is_visible(request.user): - continue - break - else: - raise Http404() - query = request.GET.get('q') or '' - sqs = SearchQuerySet().filter(SQ(content=AutoQuery(query)) | SQ(title=AutoQuery(query))) - sqs = sqs.highlight() - sqs.load_all() - hits = [] - for hit in sqs: - description = None - if hit.model_name == 'page' and hit.highlighted['text']: - description = '

%s

' % hit.highlighted['text'][0] - hits.append({ - 'text': hit.title, - 'url': hit.url, - 'description': description, - }) - - return HttpResponse(json.dumps({'data': hits}), content_type='application/json') - - def snapshot(request, *args, **kwargs): snapshot = PageSnapshot.objects.get(id=kwargs['pk']) return publish_page(request, snapshot.get_page()) diff --git a/combo/settings.py b/combo/settings.py index 8d71475e..97ce0e48 100644 --- a/combo/settings.py +++ b/combo/settings.py @@ -77,7 +77,6 @@ INSTALLED_APPS = ( 'combo.apps.pwa', 'combo.apps.gallery', 'combo.apps.kb', - 'haystack', 'xstatic.pkg.josefinsans', 'xstatic.pkg.leaflet', 'xstatic.pkg.opensans', @@ -189,13 +188,6 @@ CKEDITOR_CONFIGS = { CKEDITOR_CONFIGS['small'] = copy.copy(CKEDITOR_CONFIGS['default']) CKEDITOR_CONFIGS['small']['height'] = 150 -HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', - 'PATH': os.path.join(BASE_DIR, 'whoosh_index'), - }, -} - # from solr.thumbnail -- https://sorl-thumbnail.readthedocs.io/en/latest/reference/settings.html THUMBNAIL_PRESERVE_FORMAT = True THUMBNAIL_FORCE_OVERWRITE = False @@ -264,6 +256,7 @@ MELLON_IDENTITY_PROVIDERS = [] # search services COMBO_SEARCH_SERVICES = {} +POSTGRESQL_FTS_SEARCH_CONFIG = 'french' # mapping of payment modes LINGO_NO_ONLINE_PAYMENT_REASONS = {} diff --git a/debian/combo.cron.hourly b/debian/combo.cron.hourly index c4828684..4b3d4e9c 100644 --- a/debian/combo.cron.hourly +++ b/debian/combo.cron.hourly @@ -2,5 +2,3 @@ /sbin/runuser -u combo /usr/bin/combo-manage -- tenant_command cron --all-tenants /sbin/runuser -u combo /usr/bin/combo-manage -- tenant_command clearsessions --all-tenants -# update_index cannot be used due to some bug in haystack/whoosh (#30509) -/sbin/runuser -u combo /usr/bin/combo-manage -- tenant_command rebuild_index --noinput --all-tenants -v0 diff --git a/debian/control b/debian/control index fdad54e3..7b27f336 100644 --- a/debian/control +++ b/debian/control @@ -21,14 +21,13 @@ Depends: ${misc:Depends}, ${python3:Depends}, python3-xstatic-opensans, python3-xstatic-roboto-fontface (>= 0.5.0.0), python3-eopayment (>= 1.35), - python3-django-haystack (>= 2.4.0), python3-django-ratelimit, python3-sorl-thumbnail, python3-pil, python3-pywebpush, python3-pygal, python3-lxml -Recommends: python3-django-mellon, python3-whoosh +Recommends: python3-django-mellon Conflicts: python-lingo Breaks: combo (<< 2.34.post2) Description: Portal Management System (Python module) diff --git a/requirements.txt b/requirements.txt index c4eca6a9..fa266a1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,5 @@ XStatic_roboto-fontface eopayment>=1.13 python-dateutil djangorestframework>=3.3, <3.7 -django-haystack -whoosh sorl-thumbnail pyproj diff --git a/setup.py b/setup.py index 0696e5c0..19b94544 100644 --- a/setup.py +++ b/setup.py @@ -163,9 +163,7 @@ setup( 'eopayment>=1.41', 'python-dateutil', 'djangorestframework>=3.3, <3.7', - 'django-haystack', 'django-ratelimit<3', - 'whoosh', 'sorl-thumbnail', 'Pillow', 'pyproj', diff --git a/tests/settings.py b/tests/settings.py index 839c02b8..b7b8a479 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -44,9 +44,6 @@ COMBO_DASHBOARD_ENABLED = True import tempfile MEDIA_ROOT = tempfile.mkdtemp('combo-test') -HAYSTACK_CONNECTIONS['default']['PATH'] = os.path.join( - tempfile.mkdtemp('combo-test-whoosh')) - if 'DISABLE_MIGRATIONS' in os.environ: class DisableMigrations(object): def __contains__(self, item): diff --git a/tests/test_search.py b/tests/test_search.py index 5ed2f55c..65feffcb 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -6,17 +6,16 @@ import shutil import mock from django.conf import settings +from django.contrib.auth.models import AnonymousUser, User, Group from django.test import override_settings from django.test.client import RequestFactory from django.core.management import call_command from django.core.urlresolvers import reverse -from haystack.exceptions import SkipDocument - from combo.apps.search.engines import engines -from combo.apps.search.models import SearchCell +from combo.apps.search.models import SearchCell, IndexedCell +from combo.apps.search.utils import index_site, search_site from combo.data.models import Page, JsonCell, TextCell, MenuCell, LinkCell -from combo.data.search_indexes import PageIndex from .test_manager import login @@ -229,9 +228,9 @@ def test_search_contents(): page = Page(title='example page', slug='example-page') page.save() - # no indexation of private cells (is_visible check) + # private cells are indexed cell = TextCell(page=page, text='foobar', public=False, order=0) - assert cell.render_for_search() == '' + assert cell.render_for_search().strip() == 'foobar' # no indexation of empty cells (is_relevant check) cell = TextCell(page=page, text='', order=0) @@ -247,25 +246,20 @@ def test_search_contents(): def test_search_contents_index(): page = Page(title='example page', slug='example-page') + page.public = True page.save() - page_index = PageIndex() - assert page_index.get_model() is Page - - assert page_index.prepare_url(page) == '/example-page/' - - page_index.prepare(page) - - page.public = False - with pytest.raises(SkipDocument): - page_index.prepare(page) - - page.public = True cell = TextCell(page=page, text='

foobar

', order=0) cell.save() - prepared_data = page_index.prepare(page) - assert 'foobar' in prepared_data['text'] + request = RequestFactory().get('/') + request.user = AnonymousUser() + hits = search_site(request, 'foobar') + assert len(hits) == 0 + index_site() + hits = search_site(request, 'foobar') + assert len(hits) == 1 + def test_search_contents_technical_placeholder(): page = Page(title='example page', slug='example-page') @@ -274,10 +268,14 @@ def test_search_contents_technical_placeholder(): TextCell(page=page, text='

foobar

', order=0, placeholder='_off').save() TextCell(page=page, text='

barfoo

', order=0, placeholder='on').save() - page_index = PageIndex() - prepared_data = page_index.prepare(page) - assert 'barfoo' in prepared_data['text'] - assert not 'foobar' in prepared_data['text'] + request = RequestFactory().get('/') + request.user = AnonymousUser() + index_site() + hits = search_site(request, 'foobar') + assert len(hits) == 0 + hits = search_site(request, 'barfoo') + assert len(hits) == 1 + def test_search_api(app): page = Page(title='example page', slug='example-page') @@ -291,70 +289,61 @@ def test_search_api(app): cell = TextCell(page=second_page, text='

other baz

', order=0) cell.save() - - page_index = PageIndex() - page_index.reindex() - - resp = app.get('/api/search/?q=foobar', status=404) + index_site() cell = SearchCell(page=page, _search_services={'data': ['_text']}, order=0) cell.save() - resp = app.get('/api/search/?q=foobar', status=200) - assert len(resp.json['data']) == 1 - assert resp.json['data'][0]['text'] == 'example page' + resp = app.get('/ajax/search/%s/_text/?q=foobar' % cell.id, status=200) + assert resp.text.count('