Skip to content
This repository has been archived by the owner on Aug 26, 2022. It is now read-only.

Commit

Permalink
Merge pull request #1655 from jezdez/search-navigator-877679
Browse files Browse the repository at this point in the history
fix bug 877679 - added a document navigator stored in localStorage for MDN search traffic
  • Loading branch information
groovecoder committed Feb 12, 2014
2 parents a87b963 + 2339350 commit 5b6455e
Show file tree
Hide file tree
Showing 24 changed files with 620 additions and 140 deletions.
31 changes: 27 additions & 4 deletions apps/search/fields.py
@@ -1,15 +1,38 @@
from rest_framework import serializers


class SearchQueryField(serializers.Field):
class QueryParameterField(serializers.Field):
param_name = None
method = 'get'
empty_value = None

def to_native(self, value):
request = self.context.get('request')
getter = getattr(request.QUERY_PARAMS, self.method)
return getter(self.param_name, self.empty_value)


class SearchQueryField(QueryParameterField):
"""
Field that returns a link to the next page in paginated results.
Field that returns the search query of the current request.
"""
search_param = 'q'
param_name = 'q'


class TopicQueryField(QueryParameterField):
"""
Field that returns the topic list of the current request.
"""
param_name = 'topic'
method = 'getlist'
empty_value = []


class LocaleField(serializers.Field):

def to_native(self, value):
request = self.context.get('request')
return request.QUERY_PARAMS.get(self.search_param, None)
return request.locale


class DocumentExcerptField(serializers.Field):
Expand Down
4 changes: 2 additions & 2 deletions apps/search/filters.py
Expand Up @@ -2,8 +2,8 @@
from django.conf import settings
from elasticutils import Q
from elasticutils.contrib.django import F

from rest_framework.filters import BaseFilterBackend
from waffle import flag_is_active

from search.models import DocumentType, Filter

Expand Down Expand Up @@ -49,7 +49,7 @@ def filter_queryset(self, request, queryset, view):
boosts[operation] = boost
queryset = (queryset.query(Q(should=True, **queries))
.boost(**boosts))
if request.user.is_superuser:
if flag_is_active(request, 'search_explanation'):
queryset = queryset.explain() # adds scoring explaination
return queryset

Expand Down
15 changes: 15 additions & 0 deletions apps/search/renderers.py
@@ -0,0 +1,15 @@
from rest_framework.renderers import TemplateHTMLRenderer

from .store import ref_from_request


class ExtendedTemplateHTMLRenderer(TemplateHTMLRenderer):
template_name = 'search/results.html'

def resolve_context(self, data, request, response):
"""
Adds some more data to the template context.
"""
data['search_ref'] = ref_from_request(request)
return super(ExtendedTemplateHTMLRenderer,
self).resolve_context(data, request, response)
5 changes: 4 additions & 1 deletion apps/search/serializers.py
@@ -1,6 +1,7 @@
from rest_framework import serializers, pagination

from .fields import SearchQueryField, DocumentExcerptField
from .fields import (SearchQueryField, DocumentExcerptField,
TopicQueryField, LocaleField)
from .models import Filter


Expand Down Expand Up @@ -30,12 +31,14 @@ class SearchSerializer(pagination.PaginationSerializer):
pages = serializers.Field(source='paginator.num_pages')
start = serializers.Field(source='start_index')
end = serializers.Field(source='end_index')
locale = LocaleField(source='*')
filters = FacetedFilterSerializer(source='paginator.object_list.'
'faceted_filters',
many=True)


class DocumentSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(read_only=True, max_length=255)
slug = serializers.CharField(read_only=True, max_length=255)
locale = serializers.CharField(read_only=True, max_length=7)
Expand Down
38 changes: 38 additions & 0 deletions apps/search/store.py
@@ -0,0 +1,38 @@
import hashlib

from urlobject import URLObject as URL

from django.conf import settings
from django.utils import translation

from sumo.urlresolvers import reverse

QUERY_PARAM = 'q'
PAGE_PARAM = 'page'
TOPICS_PARAM = 'topic'


def ref_from_referer(request):
referrer = request.META.get('HTTP_REFERER', None)
if (referrer is None or
reverse('search', locale=request.locale) != URL(referrer).path):
return None
return ref_from_url(referrer)


def ref_from_request(request):
return ref_from_url(request.build_absolute_uri())


def ref_from_url(url):
url = URL(url)
query = url.query.dict.get(QUERY_PARAM, '')
page = url.query.dict.get(PAGE_PARAM, 1)
topics = url.query.multi_dict.get(TOPICS_PARAM, [])
locale = translation.get_language()

md5 = hashlib.md5()
for value in [query, page, locale] + topics:
md5.update(unicode(value))

return md5.hexdigest()[:16]
22 changes: 16 additions & 6 deletions apps/search/templates/search/results.html
Expand Up @@ -21,10 +21,10 @@
<div class="column-container">
<div class="column-1"><i aria-hidden="true" class="icon-file-text-alt"></i></div>
<div class="column-5 result-list-item">
<h4><a href="{{ doc.url }}"{% if index == 1 %} tabindex="1"{% endif %}>{{ doc.title }}</a></h4>
<h4><a href="{{ doc.url }}"{% if index == 1 %} tabindex="1"{% endif %} data-slug="{{ doc.slug }}" data-docid="{{ doc.id }}">{{ doc.title }}</a></h4>
<p>{{ doc.excerpt|safe }}</p>
<p class="search-meta"><a href="{{ doc.url }}">{{ doc.url|replace('https://', '') }}</a>
{% if user.is_superuser %}Score: <span title="explanation: {{ doc.explanation }}">{{ doc.score }}</span>{% endif%}
{% if waffle.flag('search_explanation') %}Score: <span title="explanation: {{ doc.explanation }}">{{ doc.score }}</span>{% endif%}
</p>
</div>
{# Hiding until search has this capability #}{#
Expand All @@ -51,8 +51,7 @@ <h2 class="offscreen">{{ _('Search') }}</h2>
</form>
</div>


<div class="column-container">
<div class="column-container">
<!-- left results -->
<div class="column-main">

Expand Down Expand Up @@ -128,12 +127,12 @@ <h3><i aria-hidden="true" class="icon-copy"></i>{{ _('Docs') }}</h3>

<div class="pager">
{% if previous %}
<a class="button" href="{{ previous }}">
<a class="button" href="{{ previous }}" id="search-result-previous">
{{ _('Previous') }}
</a>
{% endif %}
{% if next %}
<a class="button" href="{{ next }}">
<a class="button" href="{{ next }}" id="search-result-next">
{{ _('Next') }}
</a>
{% endif %}
Expand Down Expand Up @@ -170,3 +169,14 @@ <h3><i aria-hidden="true" class="icon-th-list"></i>{{ filter_group.name }}</h3>
{% endif %}
</div>
{% endblock %}


{% block js %}
{% if waffle.flag('search_doc_navigator') and count and search_ref %}
<script>
$(document).ready(function() {
$('.result-list-item h4 a').mozSearchStore('{{ search_ref }}');
});
</script>
{% endif %}
{% endblock js %}
10 changes: 10 additions & 0 deletions apps/search/tests/__init__.py
Expand Up @@ -9,6 +9,9 @@
from test_utils import TestCase

from devmo.tests import LocalizingMixin
from sumo.urlresolvers import reset_url_prefixer
from sumo.middleware import LocaleURLMiddleware

from search.index import get_index, get_indexing_es


Expand Down Expand Up @@ -71,6 +74,7 @@ def setUp(self):
def tearDown(self):
super(ElasticTestCase, self).tearDown()
self.teardown_indexes()
reset_url_prefixer()

def refresh(self, timesleep=0):
index = get_index()
Expand Down Expand Up @@ -111,3 +115,9 @@ def teardown_indexes(self):
# If we get this error, it means the index didn't exist
# so there's nothing to delete.
pass

def get_request(self, *args, **kwargs):
request = factory.get(*args, **kwargs)
# setting request.locale correctly
LocaleURLMiddleware().process_request(request)
return request
22 changes: 8 additions & 14 deletions apps/search/tests/test_filters.py
@@ -1,13 +1,11 @@
from nose.tools import ok_

from sumo.middleware import LocaleURLMiddleware
from waffle.models import Flag

from search.tests import ElasticTestCase, factory
from search.views import SearchView

from search.filters import (SearchQueryBackend, HighlightFilterBackend,
LanguageFilterBackend, DatabaseFilterBackend)
from search.tests import ElasticTestCase
from search.views import SearchView


class FilterTests(ElasticTestCase):
Expand All @@ -22,7 +20,7 @@ class SearchQueryView(SearchView):
filter_backends = (SearchQueryBackend,)

view = SearchQueryView.as_view()
request = factory.get('/en-US/search?q=article')
request = self.get_request('/en-US/search?q=article')
response = view(request)
self.assertEqual(response.data['count'], 4)
self.assertEqual(len(response.data['documents']), 4)
Expand All @@ -36,7 +34,7 @@ class HighlightView(SearchView):
filter_backends = (SearchQueryBackend, HighlightFilterBackend)

view = HighlightView.as_view()
request = factory.get('/en-US/search?q=article')
request = self.get_request('/en-US/search?q=article')
response = view(request)
ok_('<em>article</em>' in response.data['documents'][0]['excerpt'])

Expand All @@ -45,17 +43,13 @@ class LanguageView(SearchView):
filter_backends = (LanguageFilterBackend,)

view = LanguageView.as_view()
request = factory.get('/fr/search?q=article')
# setting request.locale correctly
LocaleURLMiddleware().process_request(request)
request = self.get_request('/fr/search?q=article')
response = view(request)
self.assertEqual(response.data['count'], 1)
self.assertEqual(len(response.data['documents']), 1)
self.assertEqual(response.data['documents'][0]['locale'], 'fr')

request = factory.get('/en-US/search?q=article')
# setting request.locale correctly
LocaleURLMiddleware().process_request(request)
request = self.get_request('/en-US/search?q=article')
response = view(request)
self.assertEqual(response.data['count'], 5)
self.assertEqual(len(response.data['documents']), 5)
Expand All @@ -66,7 +60,7 @@ class DatabaseFilterView(SearchView):
filter_backends = (DatabaseFilterBackend,)

view = DatabaseFilterView.as_view()
request = factory.get('/en-US/search?topic=tagged')
request = self.get_request('/en-US/search?topic=tagged')
response = view(request)
self.assertEqual(response.data['count'], 2)
self.assertEqual(len(response.data['documents']), 2)
Expand All @@ -88,7 +82,7 @@ class DatabaseFilterView(SearchView):
},
])

request = factory.get('/fr/search?topic=non-existent')
request = self.get_request('/fr/search?topic=non-existent')
response = view(request)
self.assertEqual(response.data['count'], 6)
self.assertEqual(len(response.data['documents']), 6)
Expand Down
23 changes: 15 additions & 8 deletions apps/search/tests/test_serializers.py
@@ -1,11 +1,10 @@
from nose.tools import ok_, eq_

from search.models import Filter, FilterGroup
from search.tests import ElasticTestCase, factory
from search.tests import ElasticTestCase

from test_utils import TestCase

from search.fields import DocumentExcerptField, SearchQueryField
from search.fields import (DocumentExcerptField, SearchQueryField,
TopicQueryField)
from search.models import DocumentType
from search.serializers import FilterSerializer, DocumentSerializer
from search.queries import DocumentS
Expand Down Expand Up @@ -41,7 +40,7 @@ def test_document_serializer(self):
eq_(dict_data['title'], 'le title')


class FieldTests(TestCase):
class FieldTests(ElasticTestCase):

def test_DocumentExcerptField(self):

Expand All @@ -59,12 +58,20 @@ class FakeValue(DocumentType):
eq_(field.to_native(FakeValue()), FakeValue.summary)

def test_SearchQueryField(self):
fake_request = factory.get('/?q=test')
request = self.get_request('/?q=test')
# APIRequestFactory doesn't actually return APIRequest objects
# but standard HttpRequest objects due to the way it initializes
# the request when APIViews are called
fake_request.QUERY_PARAMS = fake_request.GET
request.QUERY_PARAMS = request.GET

field = SearchQueryField()
field.context = {'request': fake_request}
field.context = {'request': request}
eq_(field.to_native(None), 'test')

def test_TopicQueryField(self):
request = self.get_request('/?topic=spam&topic=eggs')
request.QUERY_PARAMS = request.GET

field = TopicQueryField()
field.context = {'request': request}
eq_(field.to_native(None), ['spam', 'eggs'])

0 comments on commit 5b6455e

Please sign in to comment.