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:
parent
6d32f93452
commit
7e87726b74
@ -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;
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
59
python/src/stackdump/templates/includes/results.html.inc
Normal file
59
python/src/stackdump/templates/includes/results.html.inc
Normal 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)) }}">← Previous</a></li>
|
||||
{% else %}
|
||||
<li class="prev disabled"><a href="#">← 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) }}">→ Next</a></li>
|
||||
{% else %}
|
||||
<li class="next disabled"><a href="#">→ Next</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
@ -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)) }}">← Previous</a></li>
|
||||
{% else %}
|
||||
<li class="prev disabled"><a href="#">← 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) }}">→ Next</a></li>
|
||||
{% else %}
|
||||
<li class="next disabled"><a href="#">→ 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 }}&s={{ sort_by }}">{{ s.name }}</a>
|
||||
<small class="tagline">{{ s.dump_date }}</small>
|
||||
</h6>
|
||||
</li>
|
||||
|
58
python/src/stackdump/templates/site_results.html
Normal file
58
python/src/stackdump/templates/site_results.html
Normal 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 }}&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 }}&s={{ sort_by }}">Search across all sites.</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user