#encoding: utf-8
import datetime
+from hashlib import sha1
from django.conf import settings
from django.contrib.sites.models import Site
__all__ = (
- 'Result', 'BaseSearch', 'DatabaseSearch', 'URLSearch', 'JSONSearch', 'GoogleSearch', 'SearchRegistry', 'registry'
+ 'Result', 'BaseSearch', 'DatabaseSearch', 'URLSearch', 'JSONSearch', 'GoogleSearch', 'SearchRegistry', 'registry', 'get_search_instance'
)
-SEARCH_CACHE_KEY = 'philo_sobol_search_results'
-DEFAULT_RESULT_TEMPLATE_STRING = "{% if url %}<a href='{{ url }}'>{% endif %}{{ title }}{% if url %}</a>{% endif %}"
-DEFAULT_RESULT_TEMPLATE = Template(DEFAULT_RESULT_TEMPLATE_STRING)
-
-# Determines the timeout on the entire result cache.
-MAX_CACHE_TIMEOUT = 60*24*7
+SEARCH_CACHE_SEED = 'philo_sobol_search_results'
+USE_CACHE = getattr(settings, 'SOBOL_USE_SEARCH', True)
class RegistrationError(Exception):
registry = SearchRegistry()
+def _make_cache_key(search, search_arg):
+ return sha1(SEARCH_CACHE_SEED + search.slug + search_arg).hexdigest()
+
+
+def get_search_instance(slug, search_arg):
+ """Returns a search instance for the given slug, either from the cache or newly-instantiated."""
+ search = registry[slug]
+ search_arg = search_arg.lower()
+ if USE_CACHE:
+ key = _make_cache_key(search, search_arg)
+ cached = cache.get(key)
+ if cached:
+ return cached
+ return search(search_arg)
+
+
+
class Result(object):
"""
:class:`Result` is a helper class that, given a search and a result of that search, is able to correctly render itself with a template defined by the search. Every :class:`Result` will pass a ``title``, a ``url`` (if applicable), and the raw ``result`` returned by the search into the template context when rendering.
if 'verbose_name' not in attrs:
attrs['verbose_name'] = capfirst(' '.join(convert_camelcase(name).rsplit(' ', 1)[:-1]))
if 'slug' not in attrs:
- attrs['slug'] = name.lower()
+ attrs['slug'] = name[:-6].lower() if name.endswith("Search") else name.lower()
return super(BaseSearchMetaclass, cls).__new__(cls, name, bases, attrs)
"""
__metaclass__ = BaseSearchMetaclass
- #: The number of results to return from the complete list. Default: 10
- result_limit = 10
+ #: The number of results to return from the complete list. Default: 5
+ result_limit = 5
#: How long the items for the search should be cached (in minutes). Default: 48 hours.
_cache_timeout = 60*48
+ #: The path to the template which will be used to render the :class:`Result`\ s for this search.
+ result_template = "sobol/search/basesearch.html"
def __init__(self, search_arg):
self.search_arg = search_arg
- def _get_cached_results(self):
- """Return the cached results if the results haven't timed out. Otherwise return None."""
- result_cache = cache.get(SEARCH_CACHE_KEY)
- if result_cache and self.__class__ in result_cache and self.search_arg.lower() in result_cache[self.__class__]:
- cached = result_cache[self.__class__][self.search_arg.lower()]
- if cached['timeout'] >= datetime.datetime.now():
- return cached['results']
- return None
-
- def _set_cached_results(self, results, timeout):
- """Sets the results to the cache for <timeout> minutes."""
- result_cache = cache.get(SEARCH_CACHE_KEY) or {}
- cached = result_cache.setdefault(self.__class__, {}).setdefault(self.search_arg.lower(), {})
- cached.update({
- 'results': results,
- 'timeout': datetime.datetime.now() + datetime.timedelta(minutes=timeout)
- })
- cache.set(SEARCH_CACHE_KEY, result_cache, MAX_CACHE_TIMEOUT)
-
@property
def results(self):
"""Retrieves cached results or initiates a new search via :meth:`get_results` and caches the results."""
if not hasattr(self, '_results'):
- results = self._get_cached_results()
- if results is None:
- try:
- # Cache one extra result so we can see if there are
- # more results to be had.
- limit = self.result_limit
- if limit is not None:
- limit += 1
- results = self.get_results(limit)
- except:
- if settings.DEBUG:
- raise
- # On exceptions, don't set any cache; just return.
- return []
+ try:
+ # Cache one extra result so we can see if there are
+ # more results to be had.
+ limit = self.result_limit
+ if limit is not None:
+ limit += 1
+ results = self.get_results(limit)
+ except:
+ if settings.DEBUG:
+ raise
+ # On exceptions, don't set any cache; just return.
+ return []
- self._set_cached_results(results, self._cache_timeout)
self._results = results
+
+ if USE_CACHE:
+ key = _make_cache_key(self, self.search_arg)
+ cache.set(key, self, self._cache_timeout)
return self._results
def get_result_template(self, result):
"""Returns the template to be used for rendering the ``result``."""
- if hasattr(self, 'result_template'):
- return loader.get_template(self.result_template)
- if not hasattr(self, '_result_template'):
- self._result_template = DEFAULT_RESULT_TEMPLATE
- return self._result_template
+ return loader.get_template(self.result_template)
def get_result_extra_context(self, result):
"""Returns any extra context to be used when rendering the ``result``."""
return {}
+ @property
def has_more_results(self):
"""Returns ``True`` if there are more results than :attr:`result_limit` and ``False`` otherwise."""
return len(self.results) > self.result_limit
@property
def more_results_url(self):
- """Returns the actual url for more results. This should be accessed through :attr:`more_results_querydict` in the template so that the click can be tracked."""
- raise NotImplementedError
+ """Returns the actual url for more results. This should be accessed through :attr:`more_results_querydict` in the template so that the click can be tracked. By default, simply returns ``None``."""
+ return None
@property
def more_results_querydict(self):
"""Returns a :class:`QueryDict` for tracking whether people click on a 'more results' link."""
- return make_tracking_querydict(self.search_arg, self.more_results_url)
+ url = self.more_results_url
+ if url:
+ return make_tracking_querydict(self.search_arg, url)
+ return None
def __unicode__(self):
return self.verbose_name
class GoogleSearch(JSONSearch):
"""An example implementation of a :class:`JSONSearch`."""
search_url = "http://ajax.googleapis.com/ajax/services/search/web"
- result_template = 'search/googlesearch.html'
_cache_timeout = 60
verbose_name = "Google search (current site)"
+ result_template = "sobol/search/googlesearch.html"
+ _more_results_url = None
@property
def query_format_str(self):