577 строки
19 KiB
Python
577 строки
19 KiB
Python
from collections import defaultdict
|
|
|
|
from django.db.models import Q
|
|
from django.shortcuts import redirect
|
|
from django.utils.encoding import smart_str
|
|
|
|
import commonware.log
|
|
import jingo
|
|
from tower import ugettext as _
|
|
from mobility.decorators import mobile_template
|
|
|
|
import amo
|
|
import bandwagon.views
|
|
import browse.views
|
|
from addons.models import Addon, Category
|
|
from amo.decorators import json_view
|
|
from amo.helpers import urlparams
|
|
from amo.utils import MenuItem, sorted_groupby
|
|
from versions.compare import dict_from_int, version_int
|
|
from webapps.models import Webapp
|
|
|
|
from . import forms
|
|
from .client import (Client as SearchClient, SearchError,
|
|
CollectionsClient, PersonasClient, sphinx)
|
|
from .forms import SearchForm, SecondarySearchForm, ESSearchForm
|
|
|
|
DEFAULT_NUM_RESULTS = 20
|
|
|
|
log = commonware.log.getLogger('z.search')
|
|
|
|
|
|
def _get_versions(request, versions, version):
|
|
compats = []
|
|
url = request.get_full_path()
|
|
|
|
c = MenuItem()
|
|
(c.text, c.url) = (_('All Versions'), urlparams(url, lver=None, page=None))
|
|
|
|
if not version or version == 'any':
|
|
c.selected = True
|
|
|
|
compats.append(c)
|
|
seen = {}
|
|
exclude = request.APP.__dict__.get('exclude_versions', [])
|
|
versions.sort(reverse=True)
|
|
|
|
for v in versions:
|
|
# v is a version_int so we can get the major and minor:
|
|
v = dict_from_int(v)
|
|
if v['minor1'] == 99:
|
|
text = '%s.*' % v['major']
|
|
v_float = float('%s.99' % v['major'])
|
|
else:
|
|
text = '%s.%s' % (v['major'], v['minor1'])
|
|
v_float = float(text)
|
|
|
|
if seen.get(text):
|
|
continue
|
|
|
|
seen[text] = 1
|
|
|
|
if v_float < request.APP.min_display_version or v_float in exclude:
|
|
continue
|
|
|
|
c = MenuItem()
|
|
c.text = text
|
|
c.url = urlparams(url, lver=c.text, page=None)
|
|
|
|
if c.text == version:
|
|
c.selected = True
|
|
compats.append(c)
|
|
|
|
return compats
|
|
|
|
|
|
def _get_categories(request, categories, addon_type=None, category=None):
|
|
items = []
|
|
url = request.get_full_path()
|
|
|
|
i = MenuItem()
|
|
(i.text, i.url) = (_('All'), urlparams(url, atype=None, cat=None,
|
|
page=None))
|
|
|
|
if not addon_type and not category:
|
|
i.selected = True
|
|
|
|
items.append(i)
|
|
|
|
# Bucket the categories as addon_types so we can display them in a
|
|
# hierarchy.
|
|
bucket = defaultdict(list)
|
|
|
|
for cat in categories:
|
|
item = MenuItem()
|
|
(item.text, item.url) = (cat.name, urlparams(url, atype=None,
|
|
page=None, cat="%d,%d" % (cat.type, cat.id)))
|
|
|
|
if category == cat.id:
|
|
item.selected = True
|
|
|
|
bucket[cat.type].append(item)
|
|
|
|
for key in sorted(bucket):
|
|
children = bucket[key]
|
|
item = MenuItem()
|
|
item.children = children
|
|
(item.text, item.url) = (amo.ADDON_TYPES[key],
|
|
urlparams(url, atype=key, cat=None,
|
|
page=None))
|
|
if not category and addon_type == key:
|
|
item.selected = True
|
|
|
|
items.append(item)
|
|
|
|
return items
|
|
|
|
|
|
def _get_platforms(request, platforms, selected=None):
|
|
items = []
|
|
url = request.get_full_path()
|
|
|
|
if amo.PLATFORM_ALL.id in platforms:
|
|
platforms = amo.PLATFORMS.keys()
|
|
|
|
for platform in platforms:
|
|
if platform == amo.PLATFORM_ALL.id:
|
|
continue
|
|
item = MenuItem()
|
|
p = amo.PLATFORMS[platform]
|
|
(item.text, item.url) = (p.name,
|
|
urlparams(url, pid=(p.id or None), page=None))
|
|
if p.id == selected:
|
|
item.selected = True
|
|
items.append(item)
|
|
|
|
return items
|
|
|
|
|
|
def _get_tags(request, tags, selected):
|
|
items = []
|
|
url = request.get_full_path()
|
|
|
|
for tag in tags:
|
|
item = MenuItem()
|
|
(item.text, item.url) = (tag.tag_text.lower(),
|
|
urlparams(url, tag=tag.tag_text.encode('utf8').lower(),
|
|
page=None))
|
|
|
|
if tag.tag_text.lower() == selected:
|
|
item.selected = True
|
|
|
|
items.append(item)
|
|
|
|
return items
|
|
|
|
|
|
def _get_sort_menu(request, sort):
|
|
items = []
|
|
sorts = forms.sort_by
|
|
|
|
item = (None, _('Keyword Match'))
|
|
items.append(item)
|
|
|
|
for key, val in sorts:
|
|
if key == '':
|
|
continue
|
|
item = (key, val)
|
|
items.append(item)
|
|
|
|
return items
|
|
|
|
|
|
def _get_sorts(request, sort):
|
|
items = []
|
|
url = request.get_full_path()
|
|
|
|
sorts = forms.sort_by
|
|
|
|
item = MenuItem()
|
|
(item.text, item.url) = (_('Keyword Match'), urlparams(url, sort=None))
|
|
|
|
if not sort:
|
|
item.selected = True
|
|
|
|
items.append(item)
|
|
|
|
for key, val in sorts:
|
|
if key == '':
|
|
continue
|
|
|
|
item = MenuItem()
|
|
(item.text, item.url) = (val, urlparams(url, sort=key, page=None))
|
|
|
|
if sort == key:
|
|
item.selected = True
|
|
|
|
items.append(item)
|
|
|
|
return items
|
|
|
|
|
|
def _personas(request):
|
|
"""Handle the request for persona searches."""
|
|
form = SecondarySearchForm(request.GET)
|
|
if not form.is_valid():
|
|
log.error(form.errors)
|
|
|
|
query = form.data.get('q', '')
|
|
|
|
search_opts = {}
|
|
search_opts['limit'] = form.cleaned_data.get('pp', DEFAULT_NUM_RESULTS)
|
|
page = form.cleaned_data.get('page') or 1
|
|
search_opts['offset'] = (page - 1) * search_opts['limit']
|
|
|
|
try:
|
|
results = PersonasClient().query(query, **search_opts)
|
|
except SearchError:
|
|
return jingo.render(request, 'search/down.html', {}, status=503)
|
|
|
|
pager = amo.utils.paginate(request, results, search_opts['limit'])
|
|
categories, filter, _, _ = browse.views.personas_listing(request)
|
|
c = dict(pager=pager, form=form, categories=categories, query=query,
|
|
filter=filter)
|
|
return jingo.render(request, 'search/personas.html', c)
|
|
|
|
|
|
def _collections(request):
|
|
"""Handle the request for collections."""
|
|
form = SecondarySearchForm(request.GET)
|
|
form.is_valid()
|
|
|
|
query = form.cleaned_data.get('q', '')
|
|
|
|
search_opts = {}
|
|
search_opts['limit'] = form.cleaned_data.get('pp', DEFAULT_NUM_RESULTS)
|
|
page = form.cleaned_data.get('page') or 1
|
|
search_opts['offset'] = (page - 1) * search_opts['limit']
|
|
search_opts['sort'] = form.cleaned_data.get('sortby')
|
|
|
|
try:
|
|
results = CollectionsClient().query(query, **search_opts)
|
|
except SearchError:
|
|
return jingo.render(request, 'search/down.html', {}, status=503)
|
|
|
|
pager = amo.utils.paginate(request, results, search_opts['limit'])
|
|
c = dict(pager=pager, form=form, query=query, opts=search_opts,
|
|
filter=bandwagon.views.get_filter(request))
|
|
return jingo.render(request, 'search/collections.html', c)
|
|
|
|
|
|
@json_view
|
|
def ajax_search(request):
|
|
""" Returns a json feed of ten results for auto-complete used in
|
|
collections.
|
|
[
|
|
{"id": 123, "name": "best addon", "icon": "http://path/to/icon"},
|
|
...
|
|
]
|
|
"""
|
|
|
|
q = request.GET.get('q', '')
|
|
client = SearchClient()
|
|
try:
|
|
|
|
results = client.query('@name ' + q, limit=10,
|
|
match=sphinx.SPH_MATCH_EXTENDED2)
|
|
return [dict(id=result.id, label=unicode(result.name),
|
|
icon=result.icon_url, value=unicode(result.name).lower())
|
|
for result in results]
|
|
except SearchError:
|
|
return []
|
|
|
|
|
|
def name_query(q):
|
|
# 1. Prefer text matches first (boost=3).
|
|
# 2. Then try fuzzy matches ("fire bug" => firebug) (boost=2).
|
|
# 3. Then look for the query as a prefix of a name (boost=1.5).
|
|
# 4. Look for text matches inside the summary (boost=0.8).
|
|
# 5. Look for text matches inside the description (boost=0.3).
|
|
return dict(name__text={'query': q, 'boost': 3},
|
|
name__fuzzy={'value': q, 'boost': 2, 'prefix_length': 4},
|
|
name__startswith={'value': q, 'boost': 1.5},
|
|
summary__text={'query': q, 'boost': 0.8},
|
|
description__text={'query': q, 'boost': 0.3})
|
|
|
|
|
|
@mobile_template('search/es_results.html')
|
|
def app_search(request, template=None):
|
|
form = ESSearchForm(request.GET or {}, type=amo.ADDON_WEBAPP)
|
|
form.is_valid() # Let the form try to clean data.
|
|
query = form.cleaned_data
|
|
qs = (Webapp.search().query(or_=name_query(query['q']))
|
|
.filter(type=amo.ADDON_WEBAPP, status=amo.STATUS_PUBLIC,
|
|
is_disabled=False)
|
|
.facet(tags={'terms': {'field': 'tag'}},
|
|
categories={'terms': {'field': 'category', 'size': 100}}))
|
|
if query.get('tag'):
|
|
qs = qs.filter(tag=query['tag'])
|
|
if query.get('cat'):
|
|
qs = qs.filter(category=query['cat'])
|
|
if query.get('sort'):
|
|
mapping = {'downloads': '-weekly_downloads',
|
|
'rating': '-bayesian_rating',
|
|
'updated': '-last_updated'}
|
|
qs = qs.order_by(mapping[query['sort']])
|
|
|
|
pager = amo.utils.paginate(request, qs)
|
|
facets = pager.object_list.facets
|
|
|
|
ctx = {
|
|
'pager': pager,
|
|
'query': query,
|
|
'form': form,
|
|
'sorting': sort_sidebar(request, query, form),
|
|
'categories': category_sidebar(request, query, facets),
|
|
'tags': tag_sidebar(request, query, facets),
|
|
}
|
|
return jingo.render(request, template, ctx)
|
|
|
|
|
|
@mobile_template('search/es_results.html')
|
|
def es_search(request, tag_name=None, template=None):
|
|
APP = request.APP
|
|
types = (amo.ADDON_EXTENSION, amo.ADDON_THEME, amo.ADDON_DICT,
|
|
amo.ADDON_SEARCH, amo.ADDON_LPAPP)
|
|
|
|
fixed = fix_search_query(request.GET)
|
|
if fixed is not request.GET:
|
|
return redirect(urlparams(request.path, **fixed), permanent=True)
|
|
|
|
form = ESSearchForm(request.GET or {})
|
|
form.is_valid() # Let the form try to clean data.
|
|
|
|
category = request.GET.get('cat')
|
|
|
|
if category == 'collections':
|
|
return _collections(request)
|
|
elif category == 'personas':
|
|
return _personas(request)
|
|
|
|
query = form.cleaned_data
|
|
|
|
qs = (Addon.search().query(or_=name_query(query['q']))
|
|
.filter(status__in=amo.REVIEWED_STATUSES, is_disabled=False,
|
|
app=APP.id)
|
|
.facet(tags={'terms': {'field': 'tag'}},
|
|
platforms={'terms': {'field': 'platform'}},
|
|
appversions={'terms':
|
|
{'field': 'appversion.%s.max' % APP.id}},
|
|
categories={'terms': {'field': 'category', 'size': 100}}))
|
|
if query.get('tag'):
|
|
qs = qs.filter(tag=query['tag'])
|
|
if query.get('platform') and query['platform'] in amo.PLATFORM_DICT:
|
|
ps = (amo.PLATFORM_DICT[query['platform']].id, amo.PLATFORM_ALL.id)
|
|
qs = qs.filter(platform__in=ps)
|
|
if query.get('appver'):
|
|
# Get a min version less than X.0.
|
|
low = version_int(query['appver'])
|
|
# Get a max version greater than X.0a.
|
|
high = version_int(query['appver'] + 'a')
|
|
qs = qs.filter(**{'appversion.%s.max__gte' % APP.id: high,
|
|
'appversion.%s.min__lte' % APP.id: low})
|
|
if query.get('atype') and query['atype']in amo.ADDON_TYPES:
|
|
qs = qs.filter(type=query['atype'])
|
|
else:
|
|
qs = qs.filter(type__in=types)
|
|
if query.get('cat'):
|
|
qs = qs.filter(category=query['cat'])
|
|
if query.get('sort'):
|
|
mapping = {'users': '-average_daily_users',
|
|
'rating': '-bayesian_rating',
|
|
'updated': '-last_updated'}
|
|
qs = qs.order_by(mapping[query['sort']])
|
|
|
|
pager = amo.utils.paginate(request, qs)
|
|
facets = pager.object_list.facets
|
|
|
|
ctx = {
|
|
'pager': pager,
|
|
'query': query,
|
|
'form': form,
|
|
'sorting': sort_sidebar(request, query, form),
|
|
'categories': category_sidebar(request, query, facets),
|
|
'platforms': platform_sidebar(request, query, facets),
|
|
'versions': version_sidebar(request, query, facets),
|
|
'tags': tag_sidebar(request, query, facets),
|
|
}
|
|
return jingo.render(request, template, ctx)
|
|
|
|
|
|
@mobile_template('search/{mobile/}results.html')
|
|
def search(request, tag_name=None, template=None):
|
|
# If the form is invalid we still want to have a query.
|
|
query = request.REQUEST.get('q', '')
|
|
|
|
search_opts = {
|
|
'meta': ('versions', 'categories', 'tags', 'platforms'),
|
|
'version': None,
|
|
}
|
|
|
|
form = SearchForm(request)
|
|
form.is_valid() # Let the form try to clean data.
|
|
|
|
category = form.cleaned_data.get('cat')
|
|
|
|
if category == 'collections':
|
|
return _collections(request)
|
|
elif category == 'personas':
|
|
return _personas(request)
|
|
|
|
# TODO: Let's change the form values to something less gross when
|
|
# Remora dies in a fire.
|
|
query = form.cleaned_data['q']
|
|
|
|
addon_type = form.cleaned_data.get('atype', 0)
|
|
tag = tag_name if tag_name is not None else form.cleaned_data.get('tag')
|
|
if tag_name:
|
|
search_opts['show_personas'] = True
|
|
page = form.cleaned_data['page']
|
|
sort = form.cleaned_data.get('sort')
|
|
|
|
search_opts['version'] = form.cleaned_data.get('lver')
|
|
search_opts['limit'] = form.cleaned_data.get('pp', DEFAULT_NUM_RESULTS)
|
|
search_opts['platform'] = form.cleaned_data.get('pid', amo.PLATFORM_ALL)
|
|
search_opts['sort'] = sort
|
|
search_opts['app'] = request.APP.id
|
|
search_opts['offset'] = (page - 1) * search_opts['limit']
|
|
|
|
if category:
|
|
search_opts['category'] = category
|
|
elif addon_type:
|
|
search_opts['type'] = addon_type
|
|
|
|
search_opts['tag'] = tag
|
|
|
|
client = SearchClient()
|
|
|
|
try:
|
|
results = client.query(query, **search_opts)
|
|
except SearchError, e:
|
|
log.error('Sphinx Error: %s' % e)
|
|
return jingo.render(request, 'search/down.html', locals(), status=503)
|
|
|
|
version_filters = client.meta['versions']
|
|
|
|
# If we are filtering by a version, make sure we explicitly list it.
|
|
if search_opts['version']:
|
|
try:
|
|
version_filters += (version_int(search_opts['version']),)
|
|
except UnicodeEncodeError:
|
|
pass # We didn't want to list you anyway.
|
|
|
|
versions = _get_versions(request, client.meta['versions'],
|
|
search_opts['version'])
|
|
categories = _get_categories(request, client.meta['categories'],
|
|
addon_type, category)
|
|
tags = _get_tags(request, client.meta['tags'], tag)
|
|
platforms = _get_platforms(request, client.meta['platforms'],
|
|
search_opts['platform'])
|
|
sort_tabs = _get_sorts(request, sort)
|
|
sort_opts = _get_sort_menu(request, sort)
|
|
|
|
pager = amo.utils.paginate(request, results, search_opts['limit'])
|
|
|
|
context = dict(pager=pager, query=query, tag=tag, platforms=platforms,
|
|
versions=versions, categories=categories, tags=tags,
|
|
sort_tabs=sort_tabs, sort_opts=sort_opts, sort=sort)
|
|
return jingo.render(request, template, context)
|
|
|
|
|
|
class FacetLink(object):
|
|
|
|
def __init__(self, text, urlparams, selected=False, children=None):
|
|
self.text = text
|
|
self.urlparams = urlparams
|
|
self.selected = selected
|
|
self.children = children or []
|
|
|
|
|
|
def sort_sidebar(request, query, form):
|
|
sort = query.get('sort')
|
|
return [FacetLink(text, dict(sort=key), key == sort)
|
|
for key, text in form.fields['sort'].choices]
|
|
|
|
|
|
def category_sidebar(request, query, facets):
|
|
APP = request.APP
|
|
qatype, qcat = query.get('atype'), query.get('cat')
|
|
cats = [f['term'] for f in facets['categories']]
|
|
categories = (Category.objects.filter(id__in=cats)
|
|
# Search categories don't have an application.
|
|
.filter(Q(application=APP.id) | Q(type=amo.ADDON_SEARCH)))
|
|
if qatype in amo.ADDON_TYPES:
|
|
categories = categories.filter(type=qatype)
|
|
categories = [(atype, sorted(cats, key=lambda x: x.name))
|
|
for atype, cats in sorted_groupby(categories, 'type')]
|
|
rv = [FacetLink(_('All Add-ons'), dict(atype=None, cat=None), not qatype)]
|
|
for addon_type, cats in categories:
|
|
link = FacetLink(amo.ADDON_TYPES[addon_type],
|
|
dict(atype=addon_type, cat=None),
|
|
addon_type == qatype and not qcat)
|
|
link.children = [FacetLink(c.name, dict(atype=addon_type, cat=c.id),
|
|
c.id == qcat) for c in cats]
|
|
rv.append(link)
|
|
return rv
|
|
|
|
|
|
def version_sidebar(request, query, facets):
|
|
appver = query.get('appver')
|
|
exclude_versions = getattr(request.APP, 'exclude_versions', [])
|
|
rv = [FacetLink(_('All Versions'), dict(appver=None), not appver)]
|
|
vs = [dict_from_int(f['term']) for f in facets['appversions']]
|
|
vs = set((v['major'], v['minor1'] if v['minor1'] != 99 else 0)
|
|
for v in vs)
|
|
versions = ['%s.%s' % v for v in sorted(vs, reverse=True)]
|
|
for version, floated in zip(versions, map(float, versions)):
|
|
if (floated not in exclude_versions
|
|
and floated > request.APP.min_display_version):
|
|
rv.append(FacetLink(version, dict(appver=version),
|
|
appver == version))
|
|
|
|
return rv
|
|
|
|
|
|
def platform_sidebar(request, query, facets):
|
|
qplatform = query.get('platform')
|
|
app_platforms = request.APP.platforms.values()
|
|
ALL = app_platforms[0]
|
|
platforms = [facet['term'] for facet in facets['platforms']
|
|
if facet['term'] != ALL.id]
|
|
all_selected = not qplatform or qplatform == ALL.shortname
|
|
rv = [FacetLink(ALL.name, dict(platform=ALL.shortname), all_selected)]
|
|
for platform in app_platforms[1:]:
|
|
if platform.id in platforms:
|
|
rv.append(FacetLink(platform.name,
|
|
dict(platform=platform.shortname),
|
|
platform.shortname == qplatform))
|
|
return rv
|
|
|
|
|
|
def tag_sidebar(request, query, facets):
|
|
qtag = query.get('tag')
|
|
rv = [FacetLink(_('All Tags'), dict(tag=None), not qtag)]
|
|
tags = [facet['term'] for facet in facets['tags']]
|
|
rv += [FacetLink(tag, dict(tag=tag), tag == qtag) for tag in tags]
|
|
return rv
|
|
|
|
|
|
def fix_search_query(query):
|
|
rv = dict((smart_str(k), v) for k, v in query.items())
|
|
changed = False
|
|
# Change old keys to new names.
|
|
keys = {
|
|
'lver': 'appver',
|
|
'pid': 'platform',
|
|
}
|
|
for old, new in keys.items():
|
|
if old in query:
|
|
rv[new] = rv.pop(old)
|
|
changed = True
|
|
|
|
# Change old parameter values to new values.
|
|
params = {
|
|
'sort': {
|
|
'newest': 'updated',
|
|
'weeklydownloads': 'users',
|
|
'averagerating': 'rating',
|
|
},
|
|
'platform': dict((str(p.id), p.shortname)
|
|
for p in amo.PLATFORMS.values())
|
|
}
|
|
for key, fixes in params.items():
|
|
if key in rv and rv[key] in fixes:
|
|
rv[key] = fixes[rv[key]]
|
|
changed = True
|
|
return rv if changed else query
|