1
0
mirror of https://github.com/djohnlewis/stackdump synced 2024-12-04 23:17:37 +00:00

Implemented site-specific search.

Added site logos to searches across all sites for easier identification.
Added hints to make it more obvious which site you are searching.
Minor CSS tweaks.
This commit is contained in:
Samuel Lai 2012-02-05 17:54:13 +11:00
parent 6d32f93452
commit 7e87726b74
6 changed files with 236 additions and 133 deletions

View File

@ -105,13 +105,13 @@
#search-results li {
clear: both;
border-top: 1px solid #CCCCCC;
border-bottom: 1px solid #CCCCCC;
margin-bottom: 10px;
padding-right: 10px;
}
#search-results li:hover {
background-color: #F2F2F2;
background-color: #FAFAFA;
}
#search-results li .clearfix {
@ -119,6 +119,15 @@
height: 0;
}
#search-results li:last-child {
border-bottom: none;
}
.post-logo {
float: left;
width: 48px;
}
.post-stats-vertical {
float: left;
width: 64px;
@ -149,6 +158,10 @@
padding-left: 72px; /* 64px for post-stats-vertical + 8px for padding */
}
.post-summary-with-logo {
padding-left: 120px; /* 48px for post-logo + 64px for post-stats-vertical + 8px for padding */
}
.post-summary h3 {
line-height: normal;
padding: 7px 0;

View File

@ -217,80 +217,31 @@ def site_index(site_key):
@uses_solr
@uses_db
def search():
# TODO: scrub this first to avoid Solr injection attacks?
query = request.GET.get('q')
if not query:
redirect(settings.APP_URL_ROOT)
# the page GET parameter is zero-based
page = int(request.GET.get('p', 0))
if page < 0: page = 0
rows_per_page = int(request.GET.get('r', 10))
rows_per_page = (rows_per_page > 0) and rows_per_page or 10
sort_args = {
'newest' : 'creationDate desc',
'votes' : 'votes desc',
'relevance' : 'score desc' # score is the special keyword for the
# relevancy score in Lucene
}
sort_by = request.GET.get('s', 'relevance').lower()
# default to sorting by relevance
if sort_by not in sort_args.keys():
sort_by = 'relevance'
# perform search
results = solr_conn().search(query,
start=page*rows_per_page,
rows=rows_per_page,
sort=sort_args[sort_by])
decode_json_fields(results)
retrieve_users(results, question_only=True, ignore_comments=True)
context = { }
context['site_root_path'] = ''
context['sites'] = Site.select()
# TODO: scrub this first to avoid HTML injection attacks?
context['query'] = query
context['results'] = results
context['total_hits'] = results.hits
context['current_page'] = page + 1 # page template var is ones-based
context['rows_per_page'] = rows_per_page
context['total_pages'] = int(math.ceil(float(results.hits) / rows_per_page))
context['sort_by'] = sort_by
context.update(perform_search())
return render_template('results.html', context)
@route('/:site_key#[\w\.]+#/search')
@uses_templates
@uses_solr
@uses_db
def site_search(site_key):
context = { }
context['site_root_path'] = '%s/' % site_key
# the template uses this to allow searching on other sites
context['sites'] = Site.select()
try:
context['site'] = Site.selectBy(key=site_key).getOne()
except SQLObjectNotFound:
raise HTTPError(code=404, output='No site exists with the key %s.' % site_key)
# TODO: scrub this first to avoid Solr injection attacks?
query = request.GET.get('q')
if not query:
redirect(settings.APP_URL_ROOT)
page = request.GET.get('p', 0)
rows_per_page = request.GET.get('r', 10)
# perform search
results = solr_conn().search(query, start=page*rows_per_page, rows=rows_per_page)
decode_json_fields(results)
retrieve_users(results)
# TODO: scrub this first to avoid HTML injection attacks?
context['query'] = query
context['results'] = results
# perform the search limited by this site
context.update(perform_search(site_key))
return render_template('site_results.html', context)
@ -374,27 +325,27 @@ def retrieve_users(results, question_only=False, ignore_comments=False):
# get a list of all the user IDs
user_ids_by_site = { }
for r in results:
site_name = r['siteName']
if site_name not in user_ids_by_site.keys():
user_ids_by_site[site_name] = set()
site_key = r['siteKey']
if site_key not in user_ids_by_site.keys():
user_ids_by_site[site_key] = set()
# the search result object itself
for k in r.keys():
if k.lower().endswith('userid'):
user_ids_by_site[site_name].add(r[k])
user_ids_by_site[site_key].add(r[k])
# the question object
question = r['question']
for k in question.keys():
if k.lower().endswith('userid'):
user_ids_by_site[site_name].add(question[k])
user_ids_by_site[site_key].add(question[k])
comments = question.get('comments')
if not ignore_comments and comments:
for c in comments:
for ck in c.keys():
if ck.lower().endswith('userid'):
user_ids_by_site[site_name].add(c[ck])
user_ids_by_site[site_key].add(c[ck])
# the answers
answers = r.get('answers')
@ -402,21 +353,21 @@ def retrieve_users(results, question_only=False, ignore_comments=False):
for a in answers:
for k in a.keys():
if k.lower().endswith('userid'):
user_ids_by_site[site_name].add(a[k])
user_ids_by_site[site_key].add(a[k])
comments = a.get('comments')
if not ignore_comments and comments:
for c in comments:
for ck in c.keys():
if ck.lower().endswith('userid'):
user_ids_by_site[site_name].add(c[ck])
user_ids_by_site[site_key].add(c[ck])
# retrieve the user objects from the database by site
users_by_site = { }
for site_name in user_ids_by_site.keys():
site = Site.select(Site.q.name == site_name).getOne()
for site_key in user_ids_by_site.keys():
site = Site.select(Site.q.key == site_key).getOne()
user_objects = User.select(AND(User.q.site == site,
IN(User.q.sourceId, list(user_ids_by_site[site_name]))
IN(User.q.sourceId, list(user_ids_by_site[site_key]))
))
# convert results into a dict with user id as the key
@ -424,24 +375,24 @@ def retrieve_users(results, question_only=False, ignore_comments=False):
for u in user_objects:
users[u.sourceId] = u
users_by_site[site_name] = users
users_by_site[site_key] = users
# place user objects into the dict
for r in results:
site_name = r['siteName']
site_key = r['siteKey']
# the search result object itself
for k in r.keys():
if k.lower().endswith('userid'):
# use the same field name, minus the 'Id' on the end.
r[k[:-2]] = users_by_site[site_name].get(r[k])
r[k[:-2]] = users_by_site[site_key].get(r[k])
# the question object
question = r['question']
for k in question.keys():
if k.lower().endswith('userid'):
# use the same field name, minus the 'Id' on the end.
question[k[:-2]] = users_by_site[site_name].get(question[k])
question[k[:-2]] = users_by_site[site_key].get(question[k])
comments = question.get('comments')
if not ignore_comments and comments:
@ -449,9 +400,7 @@ def retrieve_users(results, question_only=False, ignore_comments=False):
for ck in c.keys():
if ck.lower().endswith('userid'):
# use the same field name, minus the 'Id' on the end.
c[ck[:-2]] = users_by_site[site_name].get(c[ck])
c[ck[:-2]] = users_by_site[site_key].get(c[ck])
# the answers
answers = r.get('answers')
@ -460,7 +409,7 @@ def retrieve_users(results, question_only=False, ignore_comments=False):
for k in a.keys():
if k.lower().endswith('userid'):
# use the same field name, minus the 'Id' on the end.
a[k[:-2]] = users_by_site[site_name].get(a[k])
a[k[:-2]] = users_by_site[site_key].get(a[k])
comments = a.get('comments')
if not ignore_comments and comments:
@ -468,7 +417,84 @@ def retrieve_users(results, question_only=False, ignore_comments=False):
for ck in c.keys():
if ck.lower().endswith('userid'):
# use the same field name, minus the 'Id' on the end.
c[ck[:-2]] = users_by_site[site_name].get(c[ck])
c[ck[:-2]] = users_by_site[site_key].get(c[ck])
def retrieve_sites(results):
'''\
Retrieves the site objects associated with the results.
'''
# get a list of all the site keys
site_keys = set()
for r in results:
site_keys.add(r['siteKey'])
# retrieve the site objects from the database
sites = { }
for site_key in site_keys:
sites[site_key] = Site.select(Site.q.key == site_key).getOne()
# place site objects into the dict
for r in results:
site_key = r['siteKey']
r['site'] = sites[site_key]
def perform_search(site_key=None):
'''\
Common code for performing a search and returning the context for template
rendering.
If a site_key was provided, the search will be limited to that particular
site.
'''
# TODO: scrub this first to avoid Solr injection attacks?
query = request.GET.get('q')
if not query:
redirect(settings.APP_URL_ROOT)
# this query string contains any special bits we add that we don't want
# the user to see.
int_query = query
if site_key:
int_query += ' AND siteKey:%s' % site_key
# the page GET parameter is zero-based
page = int(request.GET.get('p', 0))
if page < 0: page = 0
rows_per_page = int(request.GET.get('r', 10))
rows_per_page = (rows_per_page > 0) and rows_per_page or 10
sort_args = {
'newest' : 'creationDate desc',
'votes' : 'votes desc',
'relevance' : 'score desc' # score is the special keyword for the
# relevancy score in Lucene
}
sort_by = request.GET.get('s', 'relevance').lower()
# default to sorting by relevance
if sort_by not in sort_args.keys():
sort_by = 'relevance'
# perform search
results = solr_conn().search(int_query,
start=page*rows_per_page,
rows=rows_per_page,
sort=sort_args[sort_by])
decode_json_fields(results)
retrieve_users(results, question_only=True, ignore_comments=True)
retrieve_sites(results)
context = { }
# TODO: scrub this first to avoid HTML injection attacks?
context['query'] = query
context['results'] = results
context['total_hits'] = results.hits
context['current_page'] = page + 1 # page template var is ones-based
context['rows_per_page'] = rows_per_page
context['total_pages'] = int(math.ceil(float(results.hits) / rows_per_page))
context['sort_by'] = sort_by
return context
# END VIEW HELPERS

View File

@ -24,7 +24,7 @@
<form method="get" action="{{ SETTINGS.APP_URL_ROOT }}{{ site_root_path }}search" class="pull-right">
<input type="text" placeholder="Search" />
<input type="text" placeholder="Search {% if site %}{{ site.name }}{% endif %}" />
</form>
</div>
</div>

View File

@ -0,0 +1,59 @@
<ul id="search-results">
{% for r in results %}
<li>
{% if not site %}
<div class="post-logo">
<img src="{{ SETTINGS.APP_URL_ROOT }}media/logos/{{ r.site.key }}.png" alt="{{ r.site.name }} logo" />
</div>
{% endif %}
<div class="post-stats-vertical">
<div class="post-stat">
<p class="post-stat-value {% if r.question.score < 0 %}post-stat-value-poor{% endif %}">
{{ r.question.score }}
</p>
<p>vote{% if r.question.score != 1 %}s{% endif %}</p>
</div>
<div class="post-stat">
<p class="post-stat-value {% if r.answers|length == 0 %}post-stat-value-poor{% endif %}">
{{ r.answers|length }}
</p>
<p>answer{% if r.answers|length != 1 %}s{% endif %}</p>
</div>
</div>
<div class="post-summary {% if not site %}post-summary-with-logo{% endif %}">
<h3><a href="#">{{ r.question.title }}</a></h3>
<p>{{ r.question.body|striptags|truncate(256) }}</p>
<p class="post-details">
Asked by <strong>{{ r.question.ownerUser.displayName }}</strong> on
<strong>{{ r.question.creationDate|format_datetime }}</strong>.
</p>
<div class="post-tags">
{% for t in r.question.tags %}
<span class="label">{{ t }}</span>
{% endfor %}
</div>
</div>
{# hack to force a clear on all internal elements #}
<div class="clearfix"></div>
</li>
{% endfor %}
</ul>
<div class="pagination">
<ul>
{% if current_page > 1 %}
{# the prev page is current_page - 2 because current_page is ones-based, but the p GET parameter is zero-based #}
<li class="prev"><a href="{{ REQUEST.url|set_get_parameters('p=' ~ (current_page - 2)) }}">&larr; Previous</a></li>
{% else %}
<li class="prev disabled"><a href="#">&larr; Previous</a></li>
{% endif %}
{% for p in range(1, total_pages + 1) %}
<li {% if p == current_page %}class="active"{% endif %}><a href="{{ REQUEST.url|set_get_parameters('p=' ~ (p-1)) }}">{{ p }}</a></li>
{% endfor %}
{% if current_page != total_pages %}
{# the next page is just current_page because current_page is ones-based, but the p GET parameter is zero-based #}
<li class="next"><a href="{{ REQUEST.url|set_get_parameters('p=' ~ current_page) }}">&rarr; Next</a></li>
{% else %}
<li class="next disabled"><a href="#">&rarr; Next</a></li>
{% endif %}
</ul>
</div>

View File

@ -20,60 +20,7 @@
</div>
<div class="row">
<div class="span12">
<ul id="search-results">
{% for r in results %}
<li>
<div class="post-stats-vertical">
<div class="post-stat">
<p class="post-stat-value {% if r.question.score < 0 %}post-stat-value-poor{% endif %}">
{{ r.question.score }}
</p>
<p>vote{% if r.question.score != 1 %}s{% endif %}</p>
</div>
<div class="post-stat">
<p class="post-stat-value {% if r.answers|length == 0 %}post-stat-value-poor{% endif %}">
{{ r.answers|length }}
</p>
<p>answer{% if r.answers|length != 1 %}s{% endif %}</p>
</div>
</div>
<div class="post-summary">
<h3><a href="#">{{ r.question.title }}</a></h3>
<p>{{ r.question.body|striptags|truncate(256) }}</p>
<p class="post-details">
Asked by <strong>{{ r.question.ownerUser.displayName }}</strong> on
<strong>{{ r.question.creationDate|format_datetime }}</strong>.
</p>
<div class="post-tags">
{% for t in r.question.tags %}
<span class="label">{{ t }}</span>
{% endfor %}
</div>
</div>
{# hack to force a clear on all internal elements #}
<div class="clearfix"></div>
</li>
{% endfor %}
</ul>
<div class="pagination">
<ul>
{% if current_page > 1 %}
{# the prev page is current_page - 2 because current_page is ones-based, but the p GET parameter is zero-based #}
<li class="prev"><a href="{{ REQUEST.url|set_get_parameters('p=' ~ (current_page - 2)) }}">&larr; Previous</a></li>
{% else %}
<li class="prev disabled"><a href="#">&larr; Previous</a></li>
{% endif %}
{% for p in range(1, total_pages + 1) %}
<li {% if p == current_page %}class="active"{% endif %}><a href="{{ REQUEST.url|set_get_parameters('p=' ~ (p-1)) }}">{{ p }}</a></li>
{% endfor %}
{% if current_page != total_pages %}
{# the next page is just current_page because current_page is ones-based, but the p GET parameter is zero-based #}
<li class="next"><a href="{{ REQUEST.url|set_get_parameters('p=' ~ current_page) }}">&rarr; Next</a></li>
{% else %}
<li class="next disabled"><a href="#">&rarr; Next</a></li>
{% endif %}
</ul>
</div>
{% include 'includes/results.html.inc' %}
</div>
<div class="span4">
<div class="total-hits">
@ -92,7 +39,7 @@
<li>
<img src="{{ SETTINGS.APP_URL_ROOT }}media/logos/{{ s.key }}.png" alt="{{ s.name }} logo" />
<h6>
<a href="{{ SETTINGS.APP_URL_ROOT }}{{ s.key }}/search?q={{ query }}">{{ s.name }}</a>
<a href="{{ SETTINGS.APP_URL_ROOT }}{{ s.key }}/search?q={{ query }}&amp;s={{ sort_by }}">{{ s.name }}</a>
<small class="tagline">{{ s.dump_date }}</small>
</h6>
</li>

View File

@ -0,0 +1,58 @@
{% extends 'base.html' %}
{% block title %}Stackdump // {{ site.name }} search for "{{ query }}"{% endblock %}
{% block body %}
<div class="row">
<div class="span16">
<form id="search" method="get" action="{{ SETTINGS.APP_URL_ROOT }}search">
<input type="text" class="xlarge" name="q" value="{{ query }}" />
<input type="hidden" name="s" value="{{ sort_by }}" />
<input type="submit" class="btn primary" value="Search{% if site %} {{ site.name }}{% endif %}" />
</form>
<ul class="tabs">
<li {% if sort_by == 'newest' %}class="active"{% endif %}><a href="{{ REQUEST.url|set_get_parameters('s=newest', 'p=0') }}">newest</a></li>
<li {% if sort_by == 'votes' %}class="active"{% endif %}><a href="{{ REQUEST.url|set_get_parameters('s=votes', 'p=0') }}">votes</a></li>
<li {% if sort_by == 'relevance' %}class="active"{% endif %}><a href="{{ REQUEST.url|set_get_parameters('s=relevance', 'p=0') }}">relevance</a></li>
</ul>
</div>
</div>
<div class="row">
<div class="span12">
{% include 'includes/results.html.inc' %}
</div>
<div class="span4">
<div class="total-hits">
<p>
<span class="stat-value">{{ total_hits }}</span>
</p>
<p>
posts matched your query <em>"{{ query }}"</em>.
</p>
</div>
<div class="well site-list-container">
<h3>Search other sites</h3>
<ul class="site-list">
{% for s in sites %}
{# don't show the current site #}
{% if s.key != site.key %}
<li>
<img src="{{ SETTINGS.APP_URL_ROOT }}media/logos/{{ s.key }}.png" alt="{{ s.name }} logo" />
<h6>
<a href="{{ SETTINGS.APP_URL_ROOT }}{{ s.key }}/search?q={{ query }}&amp;s={{ sort_by }}">{{ s.name }}</a>
<small class="tagline">{{ s.dump_date }}</small>
</h6>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
<div class="well">
<a href="{{ SETTINGS.APP_URL_ROOT }}search?q={{ query }}&amp;s={{ sort_by }}">Search across all sites.</a>
</div>
</div>
</div>
{% endblock %}