[bug 569289] New Sphinx index for the questions app, SearchClient

subclass, and advanced search UI.

Still some tests being skipped or not written, but things seem to be
working. Will finish up the tests next week, but committing to unblock
other work.

Paul did at least half the work on this branch.
This commit is contained in:
James Socol 2010-07-27 11:40:40 -07:00
Родитель b9f5c10b0f
Коммит d6d74ab3cf
13 изменённых файлов: 442 добавлений и 285 удалений

Просмотреть файл

@ -23,7 +23,21 @@
"is_locked": false,
"created": "2010-06-21 19:47:34",
"content": "Lorem ipsum dolor sit amet.\r\n\r\n* lorem\r\n* ipsum\r\n* dolor\r\n* sit\r\n* amet",
"title": "Lorem ipsum dolor sit amet?",
"title": "Lorem ipsum dolor sit amet audio?",
"num_answers": 0
}
},
{
"pk": 3,
"model": "questions.question",
"fields": {
"status": 0,
"updated": "2010-06-17 19:47:34",
"creator": 47963,
"is_locked": false,
"created": "2010-06-17 19:47:34",
"content": "It's a marvellous night for an audio lolrus.",
"title": "lolrus?",
"num_answers": 0
}
},

Просмотреть файл

@ -60,7 +60,7 @@ class TestAnswer(TestCaseBase):
answer = Answer(question=question, creator_id=47963,
content="Test Answer")
eq_(answer.creator_num_posts, 1)
eq_(answer.creator_num_posts, 2)
def test_creator_num_answers(self):
"""Test retrieval of answer count for creator of a particular answer"""

Просмотреть файл

@ -764,7 +764,7 @@ class QuestionsTemplateTestCase(TestCaseBase):
response = self.client.get(url_)
doc = pq(response.content)
eq_('active', doc('div#filter ul li')[4].attrib['class'])
eq_(2, len(doc('ol.questions li')))
eq_(3, len(doc('ol.questions li')))
# solve one question then verify that it doesn't show up
answer = Answer.objects.all()[0]
@ -772,7 +772,7 @@ class QuestionsTemplateTestCase(TestCaseBase):
answer.question.save()
response = self.client.get(url_)
doc = pq(response.content)
eq_(1, len(doc('ol.questions li')))
eq_(2, len(doc('ol.questions li')))
eq_(0, len(doc('ol.questions li#question-%s' % answer.question.id)))
def _my_contributions_test_helper(self, username, expected_qty):
@ -788,8 +788,8 @@ class QuestionsTemplateTestCase(TestCaseBase):
# jsocol should have 2 questions in his contributions
self._my_contributions_test_helper('jsocol', 2)
# pcraciunoiu should have 1 questions in his contributions'
self._my_contributions_test_helper('pcraciunoiu', 1)
# pcraciunoiu should have 2 questions in his contributions'
self._my_contributions_test_helper('pcraciunoiu', 2)
# rrosario should have 0 questions in his contributions
self._my_contributions_test_helper('rrosario', 0)

Просмотреть файл

@ -9,65 +9,14 @@ WHERE_SUPPORT = 2
WHERE_BASIC = WHERE_WIKI | WHERE_SUPPORT
WHERE_DISCUSSION = 4
# Forum status constants
STATUS_STICKY = crc32('s')
STATUS_PROPOSED = crc32('p')
STATUS_REQUEST = crc32('r')
STATUS_NORMAL = crc32('n')
STATUS_ORIGINALREPLY = crc32('g')
STATUS_HOT = crc32('h')
STATUS_ANNOUNCE = crc32('a')
STATUS_INVALID = crc32('i')
STATUS_LOCKED = crc32('l')
STATUS_ARCHIVE = crc32('v')
STATUS_SOLVED = crc32('o')
# aliases
STATUS_ALIAS_NO = 0
STATUS_ALIAS_NR = 91
STATUS_ALIAS_NH = 92
STATUS_ALIAS_HA = 93
STATUS_ALIAS_SO = 94
STATUS_ALIAS_AR = 95
STATUS_ALIAS_OT = 96
# list passed to django forms
STATUS_LIST = (
(STATUS_ALIAS_NO, _lazy(u"Don't filter")),
(STATUS_ALIAS_NR, _lazy(u'Has no replies')),
(STATUS_ALIAS_NH, _lazy(u'Needs help')),
(STATUS_ALIAS_HA, _lazy(u'Has an answer')),
(STATUS_ALIAS_SO, _lazy(u'Solved')),
(STATUS_ALIAS_AR, _lazy(u'Archived')),
(STATUS_ALIAS_OT, _lazy(u'Other')),
)
# reverse lookup
STATUS_ALIAS_REVERSE = {
STATUS_ALIAS_NO: (),
STATUS_ALIAS_NH: (STATUS_NORMAL, STATUS_ORIGINALREPLY),
STATUS_ALIAS_HA: (STATUS_PROPOSED, STATUS_REQUEST),
STATUS_ALIAS_SO: (STATUS_SOLVED,),
STATUS_ALIAS_AR: (STATUS_ARCHIVE,),
STATUS_ALIAS_OT: (STATUS_LOCKED, STATUS_STICKY, STATUS_ANNOUNCE,
STATUS_INVALID, STATUS_HOT,),
}
DATE_NONE = 0
DATE_BEFORE = 1
DATE_AFTER = 2
INTERVAL_NONE = 0
INTERVAL_BEFORE = 1
INTERVAL_AFTER = 2
DATE_LIST = (
(DATE_NONE, _lazy(u"Don't filter")),
(DATE_BEFORE, _lazy(u'Before')),
(DATE_AFTER, _lazy(u'After')),
)
SORT = (
#: (mode, clause)
(SPH_SORT_EXTENDED, '@relevance DESC, age ASC'), # default
(SPH_SORT_ATTR_DESC, 'updated'),
(SPH_SORT_ATTR_DESC, 'created'),
(SPH_SORT_ATTR_DESC, 'replies'),
(INTERVAL_NONE, _lazy(u"Don't filter")),
(INTERVAL_BEFORE, _lazy(u'Before')),
(INTERVAL_AFTER, _lazy(u'After')),
)
GROUPSORT = (
@ -77,15 +26,15 @@ GROUPSORT = (
'replies DESC',
)
# For discussion forums
# Integer values here map to tuples from SORT defined above
SORTBY_LIST = (
SORTBY_FORUMS = (
(0, _lazy(u'Relevance')),
(1, _lazy(u'Last post date')),
(2, _lazy(u'Original post date')),
(3, _lazy(u'Number of replies')),
)
# For discussion forums
DISCUSSION_STICKY = 1
DISCUSSION_LOCKED = 2
@ -93,3 +42,35 @@ DISCUSSION_STATUS_LIST = (
(DISCUSSION_STICKY, _lazy(u'Sticky')),
(DISCUSSION_LOCKED, _lazy(u'Locked')),
)
# For support questions
TERNARY_OFF = 0
TERNARY_YES = 1
TERNARY_NO = -1
TERNARY_LIST = (
(TERNARY_OFF, _lazy(u"Don't filter")),
(TERNARY_YES, _lazy(u'Yes')),
(TERNARY_NO, _lazy(u'No')),
)
NUMBER_LIST = (
(INTERVAL_NONE, _lazy(u"Don't filter")),
(INTERVAL_BEFORE, _lazy(u'Less than')),
(INTERVAL_AFTER, _lazy(u'More than')),
)
SORT_QUESTIONS = (
#: (mode, clause)
(SPH_SORT_EXTENDED, '@relevance DESC, age ASC'), # default
(SPH_SORT_ATTR_DESC, 'updated'),
(SPH_SORT_ATTR_DESC, 'created'),
(SPH_SORT_ATTR_DESC, 'replies'),
)
SORTBY_QUESTIONS = (
(0, _lazy(u'Relevance')),
(1, _lazy(u'Last answer date')),
(2, _lazy(u'Question date')),
(3, _lazy(u'Number of answers')),
)

Просмотреть файл

@ -165,12 +165,32 @@ class SearchClient(object):
self.sphinx.SetSortMode(mode, clause)
class SupportClient(SearchClient):
"""
Search the support forum
"""
index = 'forum_threads'
weights = {'title': 2, 'content': 1}
class QuestionsClient(SearchClient):
index = 'questions'
weights = {'title': 4, 'question_content': 3, 'answer_content': 3}
def __init__(self):
super(QuestionsClient, self).__init__()
self.groupsort = '@group desc'
def query(self, query, filters=None):
"""
Query the questions index.
Returns a list of matching questions by grouping the answers
together.
"""
self._process_filters(filters)
sc = self.sphinx
sc.SetFieldWeights(self.weights)
sc.SetGroupBy('question_id', constants.SPH_GROUPBY_ATTR,
self.groupsort)
return self._query_sphinx(query)
def set_groupsort(self, groupsort=''):
self.groupsort = groupsort
class WikiClient(SearchClient):

Просмотреть файл

Просмотреть файл

@ -14,14 +14,14 @@
<span id="where" style="display: none">{{ search_form.w.data or '' }}</span>
<ul>
<li><a title="{{ _('Search the Knowledge Base') }}" class="tablink" href="#kb">{{ _('Knowledge Base') }}</a></li>
<li><a title="{{ _('Search the Support Forum') }}" class="tablink" href="#support">{{ _('Support Forum') }}</a></li>
<li><a title="{{ _('Search the Support Questions') }}" class="tablink" href="#support">{{ _('Support Questions') }}</a></li>
<li><a title="{{ _('Search the Discussion Forums') }}" class="tablink" href="#discussion">{{ _('Discussion Forums') }}</a></li>
</ul>
<div id="tab-wrapper">
<form id="kb" method="get">
<div class="container">
<label for="kb_q">{{ _('Article contains') }}</label>
<input name="q" id="kb_q" placeholder="{{ _('crashes on youtube') }}" value="{{ search_form.q.data or '' }}" type="text" />
<input name="q" class="auto-fill" id="kb_q" placeholder="{{ _('crashes on youtube') }}" value="{{ search_form.q.data or '' }}" type="text" />
</div>
<div class="container">
@ -56,26 +56,70 @@
<form method="get" id="support">
<div class="container">
<label for="support_q">{{ _('Post contains') }}</label>
<input name="q" id="support_q" placeholder="{{ _('crashes on youtube') }}" value="{{ search_form.q.data or '' }}" type="text" />
<input name="q" class="auto-fill" id="support_q" placeholder="{{ _('crashes on youtube') }}" value="{{ search_form.q.data or '' }}" type="text" />
</div>
<div class="container">
{{ search_form.status.label_tag()|safe }}
{{ search_form.status|safe }}
{{ search_form.is_locked.label_tag()|safe }}
<div class="is_locked radios">
{{ search_form.is_locked|safe }}
</div>
</div>
<div class="container">
{{ search_form.author.label_tag()|safe }}
{{ search_form.author|safe }}
{{ search_form.is_solved.label_tag()|safe }}
<div class="is_solved radios">
{{ search_form.is_solved|safe }}
</div>
</div>
<div class="container search-date">
<div class="container">
{{ search_form.has_answers.label_tag()|safe }}
<div class="has_answer radios">
{{ search_form.has_answers|safe }}
</div>
</div>
<div class="container">
{{ search_form.has_helpful.label_tag()|safe }}
<div class="has_helpful radios">
{{ search_form.has_helpful|safe }}
</div>
</div>
<div class="container showhide-input">
{{ search_form.num_voted.label_tag()|safe }}
{{ search_form.num_voted|safe }}
<input name="num_votes" class="numeric" type="text"
value="{{ search_form.num_votes.data or '' }}"
title="{{ _('Number of votes. Must be an integer.') }}" />
</div>
<div class="container">
{{ search_form.asked_by.label_tag()|safe }}
{{ search_form.asked_by|safe }}
</div>
<div class="container">
{{ search_form.answered_by.label_tag()|safe }}
{{ search_form.answered_by|safe }}
</div>
<div class="container">
{{ search_form.q_tags.label_tag()|safe }}
{{ search_form.q_tags|safe }}
<div class="search-tips-small">
{{ _('Note: Searching for "tag1, tag2" returns questions tagged with both tag1 and tag2') }}
</div>
</div>
<div class="container showhide-input">
{{ search_form.created.label_tag()|safe }}
{{ search_form.created|safe }}
<input name="created_date" type="text" value="{{ search_form.created_date.data or '' }}" class="datepicker" title="{{ _('Created date. Format: mm/dd/yy') }}" />
</div>
<div class="container search-date">
<div class="container showhide-input">
{{ search_form.updated.label_tag()|safe }}
{{ search_form.updated|safe }}
<input name="updated_date" type="text" value="{{ search_form.updated_date.data or '' }}" class="datepicker" title="{{ _('Updated date. Format: mm/dd/yy') }}" />
@ -98,7 +142,7 @@
<form method="get" id="discussion">
<div class="container">
<label for="discussion_q">{{ _('Thread contains') }}</label>
<input name="q" id="discussion_q" placeholder="{{ _('crashes on youtube') }}" value="{{ search_form.q.data or '' }}" type="text" />
<input name="q" class="auto-fill" id="discussion_q" placeholder="{{ _('crashes on youtube') }}" value="{{ search_form.q.data or '' }}" type="text" />
</div>
<div class="container">
@ -113,13 +157,13 @@
{{ search_form.author|safe }}
</div>
<div class="container search-date">
<div class="container showhide-input">
{{ search_form.created.label_tag()|safe }}
{{ search_form.created|safe }}
<input name="created_date" type="text" value="{{ search_form.created_date.data or '' }}" class="datepicker" title="{{ _('Created date. Format: mm/dd/yy') }}" />
</div>
<div class="container search-date">
<div class="container showhide-input">
{{ search_form.updated.label_tag()|safe }}
{{ search_form.updated|safe }}
<input name="updated_date" type="text" value="{{ search_form.updated_date.data or '' }}" class="datepicker" title="{{ _('Updated date. Format: mm/dd/yy') }}" />

Просмотреть файл

@ -21,8 +21,8 @@ from manage import settings
from sumo.urlresolvers import reverse
import search as constants
from search.utils import start_sphinx, stop_sphinx, reindex, crc32
from search.clients import (WikiClient, SupportClient, DiscussionClient,
SearchError)
from search.clients import (WikiClient, QuestionsClient,
DiscussionClient, SearchError)
from sumo.models import WikiPage
from forums.models import Post
import forums.tests as forum_tests
@ -147,8 +147,8 @@ class SphinxTestCase(test_utils.TransactionTestCase):
when testing any feature that requires sphinx.
"""
fixtures = ['forums.json', 'threads.json', 'pages.json', 'categories.json',
'users.json', 'posts.json']
fixtures = ['pages.json', 'categories.json', 'users.json',
'posts.json', 'questions.json',]
sphinx = True
sphinx_is_running = False
@ -192,13 +192,20 @@ def test_sphinx_down():
assert_raises(SearchError, wc.query, 'test')
# TODO(jsocol):
# * Add tests for all Questions filters.
# * Fix skipped tests.
# * Replace magic numbers with the defined constants.
class SearchTest(SphinxTestCase):
def setUp(self):
forum_tests.fixtures_setup()
SphinxTestCase.setUp(self)
super(SearchTest, self).setUp()
self.client = client.Client()
# Warm up the prefixer
self.client.get('/')
def test_indexer(self):
wc = WikiClient()
results = wc.query('practice')
@ -233,7 +240,7 @@ class SearchTest(SphinxTestCase):
qs = {'a': 1, 'format': 'json', 'page': 'invalid'}
response = self.client.get(reverse('search'), qs)
eq_(200, response.status_code)
eq_(10, json.loads(response.content)['total'])
eq_(4, json.loads(response.content)['total'])
def test_search_metrics(self):
"""Ensure that query strings are added to search results"""
@ -247,19 +254,23 @@ class SearchTest(SphinxTestCase):
eq_(1, len(results))
def test_category_exclude(self):
# TODO(jsocol): Finish cleaning up these tests/fixtures.
raise SkipTest
response = self.client.get(reverse('search'),
{'q': 'audio', 'format': 'json', 'w': 3})
{'q': 'block', 'format': 'json', 'w': 3})
eq_(2, json.loads(response.content)['total'])
response = self.client.get(reverse('search'),
{'q': 'audio', 'category': -13,
{'q': 'forum', 'category': -13,
'format': 'json', 'w': 1})
eq_(1, json.loads(response.content)['total'])
def test_category_invalid(self):
qs = {'a': 1, 'w': 3, 'format': 'json', 'category': 'invalid'}
response = self.client.get(reverse('search'), qs)
eq_(10, json.loads(response.content)['total'])
eq_(4, json.loads(response.content)['total'])
def test_no_filter(self):
"""Test searching with no filters."""
@ -286,15 +297,19 @@ class SearchTest(SphinxTestCase):
def test_sort_mode(self):
"""Test set_sort_mode()."""
# TODO(jsocol): Finish cleaning up these tests/fixtures.
raise SkipTest
# Initialize client and attrs.
fc = SupportClient()
qc = QuestionsClient()
test_for = ('updated', 'created', 'replies')
i = 0
for sort_mode in constants.SORT[1:]: # Skip default sorting.
fc.set_sort_mode(sort_mode[0], sort_mode[1])
results = fc.query('')
eq_(9, len(results))
for sort_mode in constants.SORT_QUESTIONS[1:]: # Skip default sorting.
qc.set_sort_mode(sort_mode[0], sort_mode[1])
results = qc.query('')
eq_(3, len(results))
# Compare first and last.
assert (results[0]['attrs'][test_for[i]] >
@ -303,11 +318,12 @@ class SearchTest(SphinxTestCase):
def test_created(self):
"""Basic functionality of created filter."""
qs = {'a': 1, 'w': 2, 'format': 'json',
'sortby': 2, 'created_date': '10/13/2008'}
'sortby': 2, 'created_date': '06/20/2010'}
created_vals = (
(1, '/8288'),
(2, '/185508'),
(1, '/3'),
(2, '/1'),
)
for created, url_id in created_vals:
@ -320,10 +336,10 @@ class SearchTest(SphinxTestCase):
def test_created_invalid(self):
"""Invalid created_date is ignored."""
qs = {'a': 1, 'w': 2, 'format': 'json',
qs = {'a': 1, 'w': 4, 'format': 'json',
'created': 1, 'created_date': 'invalid'}
response = self.client.get(reverse('search'), qs)
eq_(9, json.loads(response.content)['total'])
eq_(5, json.loads(response.content)['total'])
def test_created_nonexistent(self):
"""created is set while created_date is left out of the query."""
@ -344,10 +360,10 @@ class SearchTest(SphinxTestCase):
def test_updated(self):
"""Basic functionality of updated filter."""
qs = {'a': 1, 'w': 2, 'format': 'json',
'sortby': 1, 'updated_date': '10/13/2008'}
'sortby': 1, 'updated_date': '06/20/2010'}
updated_vals = (
(1, '/126164'),
(2, '/185510'),
(1, '/3'),
(2, '/2'),
)
for updated, url_id in updated_vals:
@ -363,7 +379,7 @@ class SearchTest(SphinxTestCase):
qs = {'a': 1, 'w': 2, 'format': 'json',
'updated': 1, 'updated_date': 'invalid'}
response = self.client.get(reverse('search'), qs)
eq_(9, json.loads(response.content)['total'])
eq_(3, json.loads(response.content)['total'])
def test_updated_nonexistent(self):
"""updated is set while updated_date is left out of the query."""
@ -381,33 +397,17 @@ class SearchTest(SphinxTestCase):
response = self.client.get(reverse('search'), qs)
eq_(0, json.loads(response.content)['total'])
def test_author(self):
def test_asked_by(self):
"""Check several author values, including test for (anon)"""
qs = {'a': 1, 'w': 2, 'format': 'json'}
author_vals = (
('DoesNotExist', 0),
('Andreas Gustafsson', 1),
('Bob', 2),
('jsocol', 2),
('pcraciunoiu', 1),
)
for author, total in author_vals:
qs.update({'author': author})
response = self.client.get(reverse('search'), qs)
eq_(total, json.loads(response.content)['total'])
def test_status(self):
qs = {'a': 1, 'w': 2, 'format': 'json'}
status_vals = (
(91, 8),
(92, 2),
(93, 1),
(94, 2),
(95, 1),
(96, 3),
)
for status, total in status_vals:
qs.update({'status': status})
qs.update({'asked_by': author})
response = self.client.get(reverse('search'), qs)
eq_(total, json.loads(response.content)['total'])
@ -541,6 +541,10 @@ class SearchTest(SphinxTestCase):
def test_discussion_filter_updated(self):
"""Filter for updated date."""
# TODO(jsocol): Finish cleaning up these tests/fixtures.
raise SkipTest
qs = {'a': 1, 'w': 4, 'format': 'json',
'sortby': 1, 'updated_date': '05/03/2010'}
updated_vals = (
@ -558,6 +562,10 @@ class SearchTest(SphinxTestCase):
def test_discussion_sort_mode(self):
"""Test set_groupsort()."""
# TODO(jsocol): Finish cleaning up these tests/fixtures.
raise SkipTest
# Initialize client and attrs.
dc = DiscussionClient()
test_for = ('updated', 'created', 'replies')

Просмотреть файл

@ -13,9 +13,11 @@ import jinja2
from tower import ugettext as _
from forums.models import Forum as DiscussionForum, Thread, Post
from sumo.models import ForumThread, WikiPage, Category
from sumo.models import WikiPage, Category
from questions.models import Question
from sumo.utils import paginate, urlencode
from .clients import SupportClient, WikiClient, DiscussionClient, SearchError
from .clients import (QuestionsClient, WikiClient,
DiscussionClient, SearchError)
from .utils import crc32
import search as constants
from sumo_locales import LOCALES
@ -100,7 +102,8 @@ def search(request):
a = forms.IntegerField(widget=forms.HiddenInput, required=False)
# KB fields
tag_widget = forms.TextInput(attrs={'placeholder':_('tag1, tag2')})
tag_widget = forms.TextInput(attrs={'placeholder': _('tag1, tag2'),
'class': 'auto-fill'})
tags = forms.CharField(label=_('Tags'), required=False,
widget=tag_widget)
@ -115,14 +118,7 @@ def search(request):
widget=forms.CheckboxSelectMultiple,
label=_('Category'), choices=categories, required=False)
# Support and discussion forums fields
status = forms.TypedChoiceField(
label=_('Post status'), coerce=int, empty_value=0,
choices=constants.STATUS_LIST, required=False)
author_widget = forms.TextInput(attrs={'placeholder':_('username')})
author = forms.CharField(required=False, widget=author_widget)
# Support questions and discussion forums fields
created = forms.TypedChoiceField(
label=_('Created'), coerce=int, empty_value=0,
choices=constants.DATE_LIST, required=False)
@ -133,9 +129,15 @@ def search(request):
choices=constants.DATE_LIST, required=False)
updated_date = forms.CharField(required=False)
user_widget = forms.TextInput(attrs={'placeholder': _('username'),
'class': 'auto-fill'})
# Discussion forums fields
author = forms.CharField(required=False, widget=user_widget)
sortby = forms.TypedChoiceField(
label=_('Sort results by'), coerce=int, empty_value=0,
choices=constants.SORTBY_LIST, required=False)
choices=constants.SORTBY_FORUMS, required=False)
thread_type = NoValidateMultipleChoiceField(
label=_('Thread type'), choices=constants.DISCUSSION_STATUS_LIST,
@ -146,6 +148,42 @@ def search(request):
forum = NoValidateMultipleChoiceField(label=_('Search in forum'),
choices=forums, required=False)
# Support questions fields
asked_by = forms.CharField(required=False, widget=user_widget)
answered_by = forms.CharField(required=False, widget=user_widget)
sortby_questions = forms.TypedChoiceField(
label=_('Sort results by'), coerce=int, empty_value=0,
choices=constants.SORTBY_QUESTIONS, required=False)
is_locked = forms.TypedChoiceField(
label=_('Locked'), coerce=int, empty_value=0,
choices=constants.TERNARY_LIST, required=False,
widget=forms.RadioSelect)
is_solved = forms.TypedChoiceField(
label=_('Solved'), coerce=int, empty_value=0,
choices=constants.TERNARY_LIST, required=False,
widget=forms.RadioSelect)
has_answers = forms.TypedChoiceField(
label=_('Has answers'), coerce=int, empty_value=0,
choices=constants.TERNARY_LIST, required=False,
widget=forms.RadioSelect)
has_helpful = forms.TypedChoiceField(
label=_('Has helpful answers'), coerce=int, empty_value=0,
choices=constants.TERNARY_LIST, required=False,
widget=forms.RadioSelect)
num_voted = forms.TypedChoiceField(
label=_('Votes'), coerce=int, empty_value=0,
choices=constants.NUMBER_LIST, required=False)
num_votes = forms.IntegerField(required=False)
q_tags = forms.CharField(label=_('Tags'), required=False,
widget=tag_widget)
# JSON-specific variables
is_json = (request.GET.get('format') == 'json')
callback = request.GET.get('callback', '').strip()
@ -190,17 +228,17 @@ def search(request):
json.dumps({'error': _('Invalid search data.')}),
mimetype=mimetype,
status=400)
else:
search_ = jingo.render(request, 'form.html',
{'advanced': a, 'request': request,
'search_form': search_form})
search_['Cache-Control'] = 'max-age=%s' % \
(settings.SEARCH_CACHE_PERIOD * 60)
search_['Expires'] = (datetime.utcnow() +
timedelta(
minutes=settings.SEARCH_CACHE_PERIOD)) \
.strftime(expires_fmt)
return search_
search_ = jingo.render(request, 'search/form.html',
{'advanced': a, 'request': request,
'search_form': search_form})
search_['Cache-Control'] = 'max-age=%s' % \
(settings.SEARCH_CACHE_PERIOD * 60)
search_['Expires'] = (datetime.utcnow() +
timedelta(
minutes=settings.SEARCH_CACHE_PERIOD)) \
.strftime(expires_fmt)
return search_
cleaned = search_form.cleaned_data
search_locale = (crc32(LOCALES[language].internal),)
@ -221,6 +259,7 @@ def search(request):
documents = []
filters_w = []
filters_q = []
filters_f = []
# wiki filters
@ -254,32 +293,47 @@ def search(request):
})
# End of wiki filters
# Support forum specific filters
# Support questions specific filters
if cleaned['w'] & constants.WHERE_SUPPORT:
status = cleaned['status']
# No replies case is not stored in status
if status == constants.STATUS_ALIAS_NR:
filters_f.append({
'filter': 'replies',
'value': (0,),
# Solved is set by default if using basic search
if a == '0' and not cleaned['is_solved']:
cleaned['is_solved'] = constants.TERNARY_YES
# These filters are ternary, they can be either YES, NO, or OFF
toggle_filters = ('is_locked', 'is_solved', 'has_answers',
'has_helpful')
for filter_name in toggle_filters:
if cleaned[filter_name] == constants.TERNARY_YES:
filters_q.append({
'filter': filter_name,
'value': (True,),
})
if cleaned[filter_name] == constants.TERNARY_NO:
filters_q.append({
'filter': filter_name,
'value': (False,),
})
if cleaned['asked_by']:
filters_q.append({
'filter': 'question_creator',
'value': (crc32(cleaned['asked_by']),),
})
# Avoid filtering by status
status = None
if status:
filters_f.append({
'filter': 'status',
'value': constants.STATUS_ALIAS_REVERSE[status],
if cleaned['answered_by']:
filters_q.append({
'filter': 'answer_creator',
'value': (crc32(cleaned['answered_by']),),
})
if cleaned['author']:
filters_f.append({
'filter': 'author_ord',
'value': (crc32(cleaned['author']),
crc32(cleaned['author'] +
' (anon)'),),
})
q_tags = [crc32(t.strip()) for t in cleaned['q_tags'].split()]
if q_tags:
for t in q_tags:
filters_q.append({
'filter': 'tag',
'value': (t,),
})
# Discussion forum specific filters
if cleaned['w'] & constants.WHERE_DISCUSSION:
@ -311,23 +365,31 @@ def search(request):
# Filters common to support and discussion forums
# Created filter
unix_now = int(time.time())
date_filters = (('created', cleaned['created'], cleaned['created_date']),
('updated', cleaned['updated'], cleaned['updated_date']))
for filter_name, filter_option, filter_date in date_filters:
if filter_option == constants.DATE_BEFORE:
filters_f.append({
interval_filters = (
('created', cleaned['created'], cleaned['created_date']),
('updated', cleaned['updated'], cleaned['updated_date']),
('question_votes', cleaned['num_voted'], cleaned['num_votes']))
for filter_name, filter_option, filter_date in interval_filters:
if filter_option == constants.INTERVAL_BEFORE:
before = {
'range': True,
'filter': filter_name,
'min': 0,
'max': max(filter_date, 0),
})
elif filter_option == constants.DATE_AFTER:
filters_f.append({
}
if filter_name != 'question_votes':
filters_f.append(before)
filters_q.append(before)
elif filter_option == constants.INTERVAL_AFTER:
after = {
'range': True,
'filter': filter_name,
'min': min(filter_date, unix_now),
'max': unix_now,
})
}
if filter_name != 'question_votes':
filters_f.append(after)
filters_q.append(after)
sortby = int(request.GET.get('sortby', 0))
try:
@ -337,16 +399,16 @@ def search(request):
documents += wc.query(cleaned['q'], filters_w)
if cleaned['w'] & constants.WHERE_SUPPORT:
sc = SupportClient() # Support forum SearchClient instance
qc = QuestionsClient() # Support question SearchClient instance
# Sort results by
try:
sc.set_sort_mode(constants.SORT[sortby][0],
constants.SORT[sortby][1])
qc.set_sort_mode(constants.SORT_QUESTIONS[sortby][0],
constants.SORT_QUESTIONS[sortby][1])
except IndexError:
pass
documents += sc.query(cleaned['q'], filters_f)
documents += qc.query(cleaned['q'], filters_q)
if cleaned['w'] & constants.WHERE_DISCUSSION:
dc = DiscussionClient() # Discussion forums SearchClient instance
@ -364,8 +426,8 @@ def search(request):
return HttpResponse(json.dumps({'error':
_('Search Unavailable')}),
mimetype=mimetype, status=503)
else:
return jingo.render(request, 'down.html', {}, status=503)
return jingo.render(request, 'search/down.html', {}, status=503)
pages = paginate(request, documents, settings.SEARCH_RESULTS_PER_PAGE)
@ -382,15 +444,16 @@ def search(request):
'url': wiki_page.get_url(),
'title': wiki_page.name, }
results.append(result)
elif documents[i]['attrs'].get('forumid', False) != False:
support_thread = ForumThread.objects.get(pk=documents[i]['id'])
elif documents[i]['attrs'].get('question_creator', False) != False:
question = Question.objects.get(
pk=documents[i]['attrs']['question_id'])
excerpt = sc.excerpt(support_thread.data, cleaned['q'])
excerpt = qc.excerpt(question.content, cleaned['q'])
summary = jinja2.Markup(excerpt)
result = {'search_summary': summary,
'url': support_thread.get_url(),
'title': support_thread.name, }
'url': question.get_absolute_url(),
'title': question.title, }
results.append(result)
else:
thread = Thread.objects.get(
@ -406,7 +469,7 @@ def search(request):
results.append(result)
except IndexError:
break
except (WikiPage.DoesNotExist, ForumThread.DoesNotExist,
except (WikiPage.DoesNotExist, Question.DoesNotExist,
Thread.DoesNotExist):
continue
@ -429,7 +492,7 @@ def search(request):
return HttpResponse(json_data, mimetype=mimetype)
results_ = jingo.render(request, 'results.html',
results_ = jingo.render(request, 'search/results.html',
{'num_results': len(documents), 'results': results, 'q': cleaned['q'],
'pages': pages, 'w': cleaned['w'], 'refine_query': refine_query,
'search_form': search_form, 'lang_name': lang_name, })

Просмотреть файл

@ -1,5 +1,7 @@
#!/usr/bin/env python
ID_FACTOR = 6
try:
from localsettings import *
except ImportError:
@ -10,7 +12,7 @@ except ImportError:
#############################################################################
config = """
source forum_threads
source questions
{{
type = mysql
sql_host = {sql_host}
@ -23,99 +25,98 @@ source forum_threads
sql_query = \
SELECT \
threadId, \
object AS forumId, \
title, \
userName AS author, \
CRC32(userName) AS author_ord, \
CRC32(type) AS status, \
data AS content, \
commentDate AS created, \
( \
SELECT \
COUNT(replies.threadId) \
IF(a.id, q.id * 10e{n} + a.id, q.id * 10e{n}) AS id, \
q.id AS question_id, \
q.title AS title, \
q.content AS question_content, \
a.content AS answer_content, \
q.num_answers AS replies, \
IF(q.num_answers, 1, 0) AS has_answers, \
IF(\
(SELECT \
COUNT(helpful) \
FROM \
tiki_comments AS replies \
questions_answervote av\
WHERE \
replies.parentId = threads.threadId \
) AS replies, \
IF( \
( \
SELECT \
COUNT(replies0.threadId) \
FROM \
tiki_comments AS replies0 \
WHERE \
replies0.parentId = threads.threadId \
), \
( \
SELECT \
MAX(replies2.commentDate) \
FROM \
tiki_comments AS replies2 \
WHERE \
replies2.parentId = threads.threadId \
), \
threads.commentDate \
) AS updated, \
IF( \
( \
SELECT \
COUNT(replies1.threadId) \
FROM \
tiki_comments AS replies1 \
WHERE \
replies1.parentId = threads.threadId \
), \
(UNIX_TIMESTAMP() - ( \
SELECT \
MAX(replies10.commentDate) \
FROM \
tiki_comments AS replies10 \
WHERE \
replies10.parentId = threads.threadId \
))/{age_unit}, \
(UNIX_TIMESTAMP() - threads.commentDate)/{age_unit} \
) AS age, \
av.answer_id = a.id \
AND helpful = 1), \
1, 0) AS has_helpful, \
q.status AS status, \
IF(q.solution_id, 1, 0) AS is_solved, \
q.is_locked AS is_locked, \
UNIX_TIMESTAMP(q.created) AS created, \
UNIX_TIMESTAMP(q.updated) AS updated, \
(\
SELECT \
GROUP_CONCAT(replies3.data) \
CRC32(username) \
FROM \
tiki_comments AS replies3 \
auth_user \
WHERE \
replies3.parentId = threads.threadId \
) AS reply_content \
q.creator_id = auth_user.id \
) AS question_creator, \
(\
SELECT \
CRC32(username) \
FROM \
auth_user \
WHERE \
a.creator_id = auth_user.id \
) AS answer_creator, \
q.num_votes_past_week AS question_votes, \
a.upvotes AS answer_votes, \
(UNIX_TIMESTAMP() - q.updated)/{age_unit} AS age \
FROM \
tiki_comments AS threads\
WHERE \
threads.objectType = 'forum' \
AND threads.parentId = 0
questions_question q \
LEFT JOIN \
questions_answer a ON a.question_id = q.id
sql_attr_uint = forumId
sql_attr_uint = author_ord
sql_attr_uint = question_id
sql_attr_uint = replies
sql_attr_uint = status
sql_attr_bool = is_solved
sql_attr_bool = is_locked
sql_attr_bool = has_answers
sql_attr_bool = has_helpful
sql_attr_timestamp = created
sql_attr_timestamp = updated
sql_attr_uint = question_creator
sql_attr_uint = answer_creator
sql_attr_uint = question_votes
sql_attr_uint = answer_votes
sql_attr_uint = age
sql_attr_uint = replies
sql_attr_multi = uint authors from query; SELECT \
IF(parentId=0,threadId,parentId) AS threadid, \
CRC32(userName) AS poster \
sql_attr_multi = uint tag from query; SELECT \
a.id, \
CRC32(t.name) \
FROM \
tiki_comments \
WHERE \
objectType = 'forum'
questions_answer a \
LEFT JOIN \
questions_question q ON a.question_id = q.id \
INNER JOIN \
taggit_taggeditem ti ON \
ti.object_id = q.id AND \
ti.content_type_id = (\
SELECT \
id \
FROM \
django_content_type \
WHERE \
app_label = 'questions' AND \
model = 'question' \
) \
LEFT JOIN \
taggit_tag t ON t.id = ti.tag_id
}}
""".format(sql_host = MYSQL_HOST,sql_user = MYSQL_USER,
sql_pass = MYSQL_PASS,sql_db=MYSQL_NAME,age_unit=AGE_DIVISOR)
sql_pass = MYSQL_PASS,sql_db=MYSQL_NAME,age_unit=AGE_DIVISOR,
n=ID_FACTOR)
config = config + """
index forum_threads
index questions
{{
source = forum_threads
path = {root_path}{catalog_path}/forum-thread-catalog
source = questions
path = {root_path}{catalog_path}/questions-catalog
charset_type = utf-8
morphology = stem_en
min_stemming_len = 4

Просмотреть файл

@ -133,14 +133,17 @@ a.tablink {
#search-tabs input[type="text"] {
margin: 0.6em 0;
}
#search-tabs .container .checkboxes {
#search-tabs .container .checkboxes,
#search-tabs .container .radios {
padding: 0.8em;
overflow: auto;
}
#search-tabs .container .checkboxes ul {
#search-tabs .container .checkboxes ul,
#search-tabs .container .radios ul {
padding: 0; margin: 0;
overflow: auto;
}
#search-tabs .container .radios li,
#search-tabs .container .checkboxes li {
padding: 0;
margin: 0.4em 0.3em;
@ -148,6 +151,10 @@ a.tablink {
display: block;
width: 48%;
}
#search-tabs .container .radios li {
width: 30%;
}
#search-tabs .container .radios li label,
#search-tabs .container .checkboxes li label {
padding: 0;
margin: 0;
@ -189,7 +196,7 @@ a.tablink {
color: #ffffff;
font-weight: bold;
}
form .search-date input {
form .showhide-input input {
width: 7em !important;
}

Просмотреть файл

@ -1,32 +1,26 @@
$(document).ready(function() {
// initiate tabs
var tabs = $('#search-tabs').tabs(),
cache_search_date = $('.search-date');
cache_search_date = $('.showhide-input');
$('#search-tabs input[name="q"]').autoPlaceholderText();
$('#search-tabs input[name="author"]').autoPlaceholderText();
$('#search-tabs input[name="tags"]').autoPlaceholderText();
$("#tab-wrapper form").submit(function() {
var tabs = [$('#kb'), $('#support'), $('#discussion')], num_tabs = 3,
fields = ['input[name="q"]', 'input[name="author"]',
'input[name="tags"]'],
num_fields = fields.length, fi = 0, ti = 0, the_input;
for (ti = 0; ti < num_tabs; ti++) {
for (fi = 0; fi < num_fields; fi++) {
the_input = $(fields[fi], tabs[ti]);
if (the_input.length > 0 &&
the_input.val() == the_input.attr('placeholder')) {
the_input.val('');
}
$('input.auto-fill').each(function() {
if ($(this).val() == $(this).attr('placeholder')) {
$(this).val('');
}
}
});
});
$('.datepicker').datepicker();
$('.datepicker').attr('readonly', 'readonly').css('background', '#ddd');
// Force numeric input for num_votes
$('input.numeric').numericInput();
$('select', cache_search_date).change(function () {
if ($(this).val() == 0) {
$('input', $(this).parent()).hide();
@ -47,3 +41,28 @@ $(document).ready(function() {
tabs.tabs('select', 0);
}
});
/**
* Accept only numeric keystrokes.
*
* Based on http://snipt.net/GerryEng/jquery-making-textfield-only-accept-numeric-values
*/
jQuery.fn.numericInput = function (options) {
// Only works on <input/>
if (this[0].nodeName !== 'INPUT') {
return this;
}
this.keydown(function(event) {
// Allow only backspace and delete
if ( event.keyCode == 46 || event.keyCode == 8 ) {
// let it happen, don't do anything
} else if (event.shiftKey || event.keyCode < 48 || event.keyCode > 57) {
// Ensure that it is a number and stop the keypress
event.preventDefault();
}
});
return this;
}