зеркало из https://github.com/mozilla/kitsune.git
[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:
Родитель
b9f5c10b0f
Коммит
d6d74ab3cf
|
@ -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;
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче