Projet

Général

Profil

0001-search-handle-several-search-engines-23534.patch

Thomas Noël, 27 août 2018 11:15

Télécharger (20 ko)

Voir les différences:

Subject: [PATCH 01/11] search: handle several search engines (#23534)

 combo/apps/search/README                      |  2 +
 .../management/commands/update_index.py       |  2 +-
 .../migrations/0002_auto_20180720_1511.py     | 22 +++++
 .../migrations/0003_create_search_services.py | 35 +++++++
 .../0004_remove_searchcell__search_service.py | 18 ++++
 combo/apps/search/models.py                   | 94 ++++++++++++-------
 .../templates/combo/search-cell-results.html  |  3 +
 .../search/templates/combo/search-cell.html   | 24 +++--
 combo/apps/search/urls.py                     |  2 +-
 combo/public/views.py                         |  2 +-
 tests/test_search.py                          | 24 ++---
 11 files changed, 168 insertions(+), 60 deletions(-)
 create mode 100644 combo/apps/search/migrations/0002_auto_20180720_1511.py
 create mode 100644 combo/apps/search/migrations/0003_create_search_services.py
 create mode 100644 combo/apps/search/migrations/0004_remove_searchcell__search_service.py
combo/apps/search/README
4 4
      'user': {
5 5
          'label': 'Search a user',
6 6
          'url': 'https://.../api/user/?q=%(q)s',
7
          # 'cache_duration': 60,  # in seconds, default is 0
8
          # 'signature': True,     # boolean, default is False
7 9
      },
8 10
  }
9 11

  
combo/apps/search/management/commands/update_index.py
30 30
            dest='skip_external_links_collection')
31 31

  
32 32
    def handle(self, **options):
33
        if SearchCell.objects.filter(_search_service='_text').count() == 0:
33
        if not any(SearchCell.get_cells_by_search_service('_text')):
34 34
            # do not index site if there's no matching search cell
35 35
            return
36 36
        if not options.get('skip_external_links_collection', False):
combo/apps/search/migrations/0002_auto_20180720_1511.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations
5
import jsonfield.fields
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('search', '0001_initial'),
12
    ]
13

  
14
    operations = [
15
        migrations.AddField(
16
            model_name='searchcell',
17
            name='_search_services',
18
            field=jsonfield.fields.JSONField(default=dict,
19
                                             verbose_name='Search Services',
20
                                             blank=True),
21
        ),
22
    ]
combo/apps/search/migrations/0003_create_search_services.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations
5

  
6

  
7
def create_search_services(apps, schema_editor):
8
    SearchCell = apps.get_model('search', 'SearchCell')
9
    for searchcell in SearchCell.objects.all():
10
        if searchcell._search_service:
11
            searchcell._search_services = {'data': [searchcell._search_service]}
12
        else:
13
            searchcell._search_services = {'data': []}
14
        searchcell.save()
15

  
16

  
17
def back_to_search_service(apps, schema_editor):
18
    SearchCell = apps.get_model('search', 'SearchCell')
19
    for searchcell in SearchCell.objects.all():
20
        if searchcell._search_services.get('data'):
21
            searchcell._search_service = searchcell._search_services.get('data')[0]
22
        else:
23
            searchcell._search_service = ''
24
        searchcell.save()
25

  
26

  
27
class Migration(migrations.Migration):
28

  
29
    dependencies = [
30
        ('search', '0002_auto_20180720_1511'),
31
    ]
32

  
33
    operations = [
34
        migrations.RunPython(create_search_services, back_to_search_service),
35
    ]
combo/apps/search/migrations/0004_remove_searchcell__search_service.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('search', '0003_create_search_services'),
11
    ]
12

  
13
    operations = [
14
        migrations.RemoveField(
15
            model_name='searchcell',
16
            name='_search_service',
17
        ),
18
    ]
combo/apps/search/models.py
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
from django.conf import settings
18
from django.db import models
19 18
from django.utils.translation import ugettext_lazy as _
20 19
from django import template
21 20
from django.http import HttpResponse
22 21
from django.core.exceptions import PermissionDenied
23 22
from django.core.urlresolvers import reverse
24
from django.forms import models as model_forms, Select
25 23
from django.utils.http import quote
26 24

  
25
from jsonfield import JSONField
26

  
27 27
from combo.utils import requests
28 28
from combo.data.models import CellBase
29 29
from combo.data.library import register_cell_class
30
from combo.utils import NothingInCacheException, get_templated_url
30
from combo.utils import get_templated_url
31

  
31 32

  
32 33
@register_cell_class
33 34
class SearchCell(CellBase):
34 35
    template_name = 'combo/search-cell.html'
35 36

  
36
    _search_service = models.CharField(verbose_name=_('Search Service'), max_length=64)
37
    _search_services = JSONField(_('Search Services'), default=dict, blank=True)
37 38

  
38 39
    class Meta:
39 40
        verbose_name = _('Search')
40 41

  
41 42
    def is_visible(self, user=None):
42
        if not self.search_service:
43
        if not self.search_services:
43 44
            return False
44 45
        return super(SearchCell, self).is_visible(user=user)
45 46

  
46
    def get_default_form_class(self):
47
        search_services = [(None, _('Not configured'))]
48
        search_services.append(('_text', _('Page Contents')))
49
        search_services.extend([(code, service['label'])
50
                for code, service in settings.COMBO_SEARCH_SERVICES.items()])
51
        widgets = {'_search_service': Select(choices=search_services)}
52
        return model_forms.modelform_factory(self.__class__,
53
                fields=['_search_service'],
54
                widgets=widgets)
55

  
56 47
    @property
57 48
    def varname(self):
58 49
        if self.slug:
......
61 52
        return ''
62 53

  
63 54
    @property
64
    def search_service(self):
65
        if self._search_service == '_text':
66
            return {'url': reverse('api-search') + '?q=%(q)s', 'label': _('Page Contents')}
67
        return settings.COMBO_SEARCH_SERVICES.get(self._search_service) or {}
55
    def search_services(self):
56
        services = []
57
        for service_slug in self._search_services.get('data') or []:
58
            if service_slug == '_text':
59
                service = {'url': reverse('api-search') + '?q=%(q)s', 'label': _('Page Contents')}
60
            else:
61
                service = settings.COMBO_SEARCH_SERVICES.get(service_slug)
62
            if service and service.get('url'):
63
                service['slug'] = service_slug
64
                services.append(service)
65
        return services
66

  
67
    @property
68
    def has_multiple_search_services(self):
69
        return len(self._search_services.get('data') or []) > 1
70

  
71
    @classmethod
72
    def get_cells_by_search_service(cls, search_service):
73
        for cell in cls.objects.all():
74
            if search_service in (cell._search_services.get('data') or []):
75
                yield cell
68 76

  
69 77
    def modify_global_context(self, context, request):
70 78
        # if self.varname is in the query string (of the page),
......
94 102
        return extra_context
95 103

  
96 104
    @classmethod
97
    def ajax_results_view(cls, request, cell_pk):
105
    def ajax_results_view(cls, request, cell_pk, service_slug):
98 106
        cell = cls.objects.get(pk=cell_pk)
99 107
        if not cell.is_visible(request.user) or not cell.page.is_visible(request.user):
100 108
            raise PermissionDenied
101 109

  
102
        query = request.GET.get('q')
103
        if query and cell.search_service.get('url'):
104
            url = cell.search_service.get('url')
105
            url = get_templated_url(url)
106
            url = url % {'q': quote(query.encode('utf-8'))}
107
            if url.startswith('/'):
108
                url = request.build_absolute_uri(url)
109
            results = requests.get(url, cache_duration=0).json()
110
        def render_response(service={}, results={'err': 0, 'data': []}):
111
            template_names = ['combo/search-cell-results.html']
112
            if cell.slug:
113
                template_names.insert(0, 'combo/cells/%s/search-cell-results.html' % cell.slug)
114
            tmpl = template.loader.select_template(template_names)
115
            context = {'cell': cell, 'results': results, 'search_service': service}
116
            return HttpResponse(tmpl.render(context, request), content_type='text/html')
117

  
118
        for service in cell.search_services:
119
            if service.get('slug') == service_slug:
120
                break
110 121
        else:
111
            results = {'err': 0, 'data': []}
112

  
113
        template_names = ['combo/search-cell-results.html']
114
        if cell.slug:
115
            template_names.insert(0, 'combo/cells/%s/search-cell-results.html' % cell.slug)
116
        tmpl = template.loader.select_template(template_names)
117
        context= {'cell': cell, 'results': results}
118
        return HttpResponse(tmpl.render(context, request), content_type='text/html')
122
            return render_response()
123

  
124
        query = request.GET.get('q')
125
        if not query:
126
            return render_response(service)
127

  
128
        url = get_templated_url(service['url'], context={'q': query, 'search_service': service})
129
        url = url % {'q': quote(query.encode('utf-8'))}  # if url contains %(q)s
130
        if url.startswith('/'):
131
            url = request.build_absolute_uri(url)
132

  
133
        if not url:
134
            return render_response(service)
135

  
136
        kwargs = {}
137
        kwargs['cache_duration'] = service.get('cache_duration', 0)
138
        kwargs['remote_service'] = 'auto' if service.get('signature') else None
139
        results = requests.get(url, **kwargs).json()
140
        return render_response(service, results)
combo/apps/search/templates/combo/search-cell-results.html
1
{% if results.data %}
2
{% if cell.has_multiple_search_services %}<p class="search-service-label">{{ search_service.label }}</p>{% endif %}
1 3
<div class="links-list">
2 4
<ul>
3 5
  {% for item in results.data %}
......
7 9
  {% endfor %}
8 10
</ul>
9 11
</div>
12
{% endif %}
combo/apps/search/templates/combo/search-cell.html
2 2
{% block cell-content %}
3 3

  
4 4
<form id="combo-search-form-{{ cell.pk }}" class="combo-search-form">
5
  <input type="text" name="q" autocomplete="off" id="combo-search-input-{{ cell.pk }}" class="combo-search-input"
6
    data-autocomplete-json="{% url 'combo-search-ajax-results' cell_pk=cell.pk %}{% if initial_query_string %}?{{ initial_query_string }}{% endif %}" />{# initial_query_string pass some context to ajax call #}
5
  <input type="text" name="q" autocomplete="off" id="combo-search-input-{{ cell.pk }}" class="combo-search-input" />
7 6
  <button class="submit-button">{% trans "Search" %}</button>
8 7
</form>
9 8

  
10
<div id="combo-search-results-{{ cell.pk }}" class="combo-search-results"></div>
9
{% for search_service in cell.search_services %}
10
<div id="combo-search-results-{{ cell.pk }}-{{ forloop.counter }}" class="combo-search-results combo-search-results-{{ search_service.slug }}"></div>
11
{% endfor %}
11 12

  
12 13
<script>
13 14
$(function() {
14 15
  var combo_search_timeout_{{ cell.pk }};
15 16
  var combo_search_form_{{ cell.pk }} = $('#combo-search-form-{{ cell.pk }}');
16 17
  var combo_search_input_{{ cell.pk }} = $('#combo-search-input-{{ cell.pk }}');
17
  var combo_search_results_{{ cell.pk }} = $('#combo-search-results-{{ cell.pk }}');
18
  var xhr = null;
18
  {% for search_service in cell.search_services %}
19
  var combo_search_results_{{ cell.pk }}_{{ forloop.counter }} = $('#combo-search-results-{{ cell.pk }}-{{ forloop.counter }}');
20
  var xhr_{{ forloop.counter }} = null;
21
  var url_{{ forloop.counter }} = '{% url 'combo-search-ajax-results' cell_pk=cell.pk service_slug=search_service.slug %}{% if initial_query_string %}?{{ intial_query_string }}{% endif %}';
22
  {% endfor %}
19 23

  
20 24
  function combo_search_update_{{ cell.pk }}() {
21
    if (xhr) xhr.abort();
22
    xhr = $.get(combo_search_input_{{ cell.pk }}.data('autocomplete-json'),
25
    {% for search_service in cell.search_services %}
26
    if (xhr_{{ forloop.counter }}) xhr_{{ forloop.counter }}.abort();
27
    xhr_{{ forloop.counter }} = $.get(url_{{ forloop.counter }},
23 28
      {'q': combo_search_input_{{ cell.pk }}.val()},
24 29
      function (response) {
25
        xhr = null;
26
        combo_search_results_{{ cell.pk }}.html(response);
30
        xhr_{{ forloop.counter }} = null;
31
        combo_search_results_{{ cell.pk }}_{{ forloop.counter }}.html(response);
27 32
      }
28 33
    );
34
    {% endfor %}
29 35
  };
30 36

  
31 37
  combo_search_input_{{ cell.pk }}.on('paste keyup', function() {
combo/apps/search/urls.py
19 19
from .models import SearchCell
20 20

  
21 21
urlpatterns = [
22
    url(r'^ajax/search/(?P<cell_pk>\w+)/$', SearchCell.ajax_results_view,
22
    url(r'^ajax/search/(?P<cell_pk>\w+)/(?P<service_slug>[\w-]+)/$', SearchCell.ajax_results_view,
23 23
        name='combo-search-ajax-results'),
24 24
]
combo/public/views.py
491 491

  
492 492

  
493 493
def api_search(request):
494
    for cell in SearchCell.objects.filter(_search_service='_text'):
494
    for cell in SearchCell.get_cells_by_search_service('_text'):
495 495
        if not cell.is_visible(request.user):
496 496
            continue
497 497
        break
tests/test_search.py
48 48
        page.save()
49 49

  
50 50
        cell = SearchCell(page=page, placeholder='content', order=0)
51
        cell._search_service = 'search1'
51
        cell._search_services = {'data': ['search1']}
52 52
        cell.save()
53 53

  
54 54
        resp = cell.render({})
......
66 66
            mock_json = mock.Mock()
67 67
            mock_json.json.return_value = response
68 68
            requests_get.return_value = mock_json
69
            resp = app.get('/ajax/search/%s/?q=foo' % cell.pk, status=200)
69
            resp = app.get('/ajax/search/%s/search1/?q=foo' % cell.pk, status=200)
70 70
            assert requests_get.call_args[0][0] == 'http://www.example.net/search/?q=foo'
71 71
            assert '<li>' not in resp.text
72 72

  
73
            resp = app.get('/ajax/search/%s/?q=foo%%23bar' % cell.pk, status=200)
73
            resp = app.get('/ajax/search/%s/search1/?q=foo%%23bar' % cell.pk, status=200)
74 74
            assert requests_get.call_args[0][0] == 'http://www.example.net/search/?q=foo%23bar'
75 75
            assert '<li>' not in resp.text
76 76

  
77 77
            response['data'] = [{'url': 'http://test', 'text': 'barbarbar'}]
78
            resp = app.get('/ajax/search/%s/?q=foo' % cell.pk, status=200)
78
            resp = app.get('/ajax/search/%s/search1/?q=foo' % cell.pk, status=200)
79 79
            assert resp.text.count('<li>') == 1
80 80
            assert '<li><a href="http://test">barbarbar</a>' in resp.text
81 81

  
82 82
            response['data'] = [{'url': 'http://test', 'text': 'barbarbar',
83 83
                                 'description': 'this is <b>html</b>'}]
84
            resp = app.get('/ajax/search/%s/?q=foo' % cell.pk, status=200)
84
            resp = app.get('/ajax/search/%s/search1/?q=foo' % cell.pk, status=200)
85 85
            assert resp.text.count('<li>') == 1
86 86
            assert '<li><a href="http://test">barbarbar</a>' in resp.text
87 87
            assert 'this is <b>html</b>' in resp.text
88 88

  
89 89
        with override_settings(TEMPLATE_VARS=TEMPLATE_VARS):
90
            cell._search_service = 'search_tmpl'
90
            cell._search_services = {'data': ['search_tmpl']}
91 91
            cell.save()
92 92
            with mock.patch('combo.apps.search.models.requests.get') as requests_get:
93 93
                response = {'err': 0, 'data': []}
94 94
                mock_json = mock.Mock()
95 95
                mock_json.json.return_value = response
96 96
                requests_get.return_value = mock_json
97
                resp = app.get('/ajax/search/%s/?q=foo' % cell.pk, status=200)
97
                resp = app.get('/ajax/search/%s/search_tmpl/?q=foo' % cell.pk, status=200)
98 98
                assert requests_get.call_args[0][0] == 'http://search.example.net/?q=foo'
99 99

  
100 100
                # TEMPLATE_VARS are accessible in template
......
103 103
                templates_settings = [settings.TEMPLATES[0].copy()]
104 104
                templates_settings[0]['DIRS'] = ['%s/templates-1' % os.path.abspath(os.path.dirname(__file__))]
105 105
                with override_settings(TEMPLATES=templates_settings):
106
                    resp = app.get('/ajax/search/%s/?q=bar' % cell.pk, status=200)
106
                    resp = app.get('/ajax/search/%s/search_tmpl/?q=bar' % cell.pk, status=200)
107 107
                    assert requests_get.call_args[0][0] == 'http://search.example.net/?q=bar'
108 108
                    assert 'searchfoo results.data=[]' in resp.text
109 109
                    assert 'search_url=http://search.example.net/' in resp.text
......
113 113
        page = Page(title='Search', slug='search_page', template_name='standard')
114 114
        page.save()
115 115
        cell = SearchCell(page=page, placeholder='content', order=0)
116
        cell._search_service = 'search1'
116
        cell._search_services = {'data': ['search1']}
117 117
        cell.save()
118 118
        assert cell.varname == ''
119 119

  
......
143 143
        cell = SearchCell(page=page, order=0)
144 144
        assert not cell.is_visible()
145 145

  
146
        cell._search_service = '_text'
146
        cell._search_services = {'data': ['_text']}
147 147
        assert cell.is_visible()
148 148

  
149 149
def test_search_contents():
......
218 218

  
219 219
    resp = app.get('/api/search/?q=foobar', status=404)
220 220

  
221
    cell = SearchCell(page=page, _search_service='_text', order=0)
221
    cell = SearchCell(page=page, _search_services={'data': ['_text']}, order=0)
222 222
    cell.save()
223 223

  
224 224
    resp = app.get('/api/search/?q=foobar', status=200)
......
242 242
    page = Page(title='example page', slug='example-page')
243 243
    page.save()
244 244

  
245
    cell = SearchCell(page=page, _search_service='_text', order=0)
245
    cell = SearchCell(page=page, _search_services={'data': ['_text']}, order=0)
246 246
    cell.save()
247 247

  
248 248
    call_command('update_index')
249
-