* :class:`.Attribute`\ s are arbitrary key/value pairs which can be attached to most of the models that Philo provides. Attributes of a :class:`.Node` will be inherited by all of the :class:`.Node`'s descendants and will be available in the template's context.
The :ttag:`~philo.templatetags.containers.container` template tag that Philo provides makes it easy to mark areas in a template which need to be editable page-by-page; every :class:`.Page` will have an additional field in the admin for each :ttag:`~philo.templatetags.containers.container` in the template it uses.
+
+How's that different than other CMSes?
+++++++++++++++++++++++++++++++++++++++
+
+Philo developed according to principles that grew out of the observation of the limitations and practices of other content management systems. For example, Philo believes that:
+
+* Designers are in charge of how content is displayed, not end users. For example, users should be able to embed images in blog entries -- but the display of the image, even the presence or absence of a wrapping ``<figure>`` element, should depend on the template used to render the entry, not the HTML5 knowledge of the user.
+ .. seealso:: :ttag:`~philo.templatetags.embed.embed`
+* Interpretation of content (as a django template, as markdown, as textile, etc.) is the responsibility of the template designer, not of code developers or the framework.
+ .. seealso:: :ttag:`~philo.templatetags.include_string.include_string`
+* Page content should be simple -- not reorderable. Each piece of content should only be related to one page. Any other system will cause more trouble than it's worth.
+ .. seealso:: :class:`.Contentlet`, :class:`.ContentReference`
+* Some pieces of information may be shared by an entire site, used in disparate places, and changed frequently enough that it is far too difficult to track down every use. These pieces of information should be stored separately from the content that contains them.
+ .. seealso:: :class:`.Attribute`
"""
-Sobol implements a generic search interface, which can be used to search databases or websites. No assumptions are made about the search method, and the results are cached using django's caching.
+Sobol implements a generic search interface, which can be used to search databases or websites. No assumptions are made about the search method. If SOBOL_USE_CACHE is ``True`` (default), the results will be cached using django's cache framework. Be aware that this may use a large number of cache entries, as a unique entry will be made for each search string for each type of search.
+
+Settings
+--------
+
+:setting:`SOBOL_USE_CACHE`
+ Whether sobol will use django's cache framework. Defaults to ``True``; this may cause a lot of entries in the cache.
+
+:setting:`SOBOL_USE_EVENTLET`
+ If :mod:`eventlet` is installed and this setting is ``True``, sobol web searches will use :mod:`eventlet.green.urllib2` instead of the built-in :mod:`urllib2` module. Default: ``False``.
+
+Templates
+---------
+
+For convenience, :mod:`.sobol` provides a template at ``sobol/search/_list.html`` which can be used with an ``{% include %}`` tag inside a full search page template to list the search results. The ``_list.html`` template also uses a basic jQuery script (``static/sobol/ajax_search.js``) to handle AJAX search result loading if the AJAX API of the current :class:`.SearchView` is enabled. If you want to use ``_list.html``, but want to provide your own version of jQuery or your own AJAX loading script, or if you want to include the basic script somewhere else (like inside the ``<head>``) simply do the following::
+
+ {% include "sobol/search/_list.html" with suppress_scripts=1 %}
"""
search_fields = ['string', 'result_urls__url']
actions = ['results_action']
if 'grappelli' in settings.INSTALLED_APPS:
- results_template = 'admin/sobol/search/grappelli_results.html'
- else:
- results_template = 'admin/sobol/search/results.html'
-
- def get_urls(self):
- urlpatterns = super(SearchAdmin, self).get_urls()
-
- def wrap(view):
- def wrapper(*args, **kwargs):
- return self.admin_site.admin_view(view)(*args, **kwargs)
- return update_wrapper(wrapper, view)
-
- info = self.model._meta.app_label, self.model._meta.module_name
-
- urlpatterns = patterns('',
- url(r'^results/$', wrap(self.results_view), name="%s_%s_selected_results" % info),
- url(r'^(.+)/results/$', wrap(self.results_view), name="%s_%s_results" % info)
- ) + urlpatterns
- return urlpatterns
+ change_form_template = 'admin/sobol/search/grappelli_change_form.html'
def unique_urls(self, obj):
return obj.unique_urls
def queryset(self, request):
qs = super(SearchAdmin, self).queryset(request)
return qs.annotate(total_clicks=Count('result_urls__clicks', distinct=True), unique_urls=Count('result_urls', distinct=True))
-
- def results_action(self, request, queryset):
- info = self.model._meta.app_label, self.model._meta.module_name
- if len(queryset) == 1:
- return HttpResponseRedirect(reverse("admin:%s_%s_results" % info, args=(queryset[0].pk,)))
- else:
- url = reverse("admin:%s_%s_selected_results" % info)
- return HttpResponseRedirect("%s?ids=%s" % (url, ','.join([str(item.pk) for item in queryset])))
- results_action.short_description = "View results for selected %(verbose_name_plural)s"
-
- def results_view(self, request, object_id=None, extra_context=None):
- if object_id is not None:
- object_ids = [object_id]
- else:
- object_ids = request.GET.get('ids').split(',')
-
- if object_ids is None:
- raise Http404
-
- qs = self.queryset(request).filter(pk__in=object_ids)
- opts = self.model._meta
-
- if len(object_ids) == 1:
- title = _(u"Search results for %s" % qs[0])
- else:
- title = _(u"Search results for multiple objects")
-
- context = {
- 'title': title,
- 'queryset': qs,
- 'opts': opts,
- 'root_path': self.admin_site.root_path,
- 'app_label': opts.app_label
- }
- return render_to_response(self.results_template, context, context_instance=RequestContext(request))
class SearchViewAdmin(EntityAdmin):
from django.utils import simplejson as json
from django.utils.datastructures import SortedDict
-from philo.contrib.sobol import registry
+from philo.contrib.sobol import registry, get_search_instance
from philo.contrib.sobol.forms import SearchForm
from philo.contrib.sobol.utils import HASH_REDIRECT_GET_KEY, URL_REDIRECT_GET_KEY, SEARCH_ARG_GET_KEY, check_redirect_hash, RegistryIterator
from philo.exceptions import ViewCanNotProvideSubpath
self._favored_results += subresults
else:
break
+ if len(self._favored_results) == len(results):
+ self._favored_results = []
return self._favored_results
class Meta:
except ImportError:
pass
else:
- add_introspection_rules([], ["^philo\.contrib\.shipherd\.models\.RegistryChoiceField"])
+ add_introspection_rules([], ["^philo\.contrib\.sobol\.models\.RegistryChoiceField"])
class SearchView(MultiView):
)
return urlpatterns
- def get_search_instance(self, slug, search_string):
- """Returns an instance of the :class:`.BaseSearch` subclass corresponding to ``slug`` in the :class:`.SearchRegistry` and instantiated with ``search_string``."""
- return registry[slug](search_string.lower())
-
def results_view(self, request, extra_context=None):
"""
Renders :attr:`results_page` with a context containing an instance of :attr:`search_form`. If the form was submitted and was valid, then one of two things has happened:
search_instances = []
for slug in self.searches:
- search_instance = self.get_search_instance(slug, search_string)
- search_instances.append(search_instance)
+ if slug in registry:
+ search_instance = get_search_instance(slug, search_string)
+ search_instances.append(search_instance)
- if self.enable_ajax_api:
- search_instance.ajax_api_url = "%s?%s=%s" % (self.reverse('ajax_api_view', kwargs={'slug': slug}, node=request.node), SEARCH_ARG_GET_KEY, search_string)
+ if self.enable_ajax_api:
+ search_instance.ajax_api_url = "%s?%s=%s" % (self.reverse('ajax_api_view', kwargs={'slug': slug}, node=request.node), SEARCH_ARG_GET_KEY, search_string)
if eventlet and not self.enable_ajax_api:
pool = eventlet.GreenPool()
pool.waitall()
context.update({
- 'searches': search_instances
+ 'searches': search_instances,
+ 'favored_results': []
})
+
+ try:
+ search = Search.objects.get(string=search_string)
+ except Search.DoesNotExist:
+ pass
+ else:
+ context['favored_results'] = [r.url for r in search.get_favored_results()]
else:
form = SearchForm()
def ajax_api_view(self, request, slug, extra_context=None):
"""
- Returns a JSON string containing two keyed lists.
+ Returns a JSON object containing the following variables:
+ search
+ Contains the slug for the search.
results
Contains the results of :meth:`.Result.get_context` for each result.
rendered
hasMoreResults
``True`` or ``False`` whether the search has more results according to :meth:`BaseSearch.has_more_results`
moreResultsURL
- Contains None or a querystring which, once accessed, will note the :class:`Click` and redirect the user to a page containing more results.
+ Contains ``None`` or a querystring which, once accessed, will note the :class:`Click` and redirect the user to a page containing more results.
"""
search_string = request.GET.get(SEARCH_ARG_GET_KEY)
- if not request.is_ajax() or not self.enable_ajax_api or slug not in self.searches or search_string is None:
+ if not request.is_ajax() or not self.enable_ajax_api or slug not in registry or slug not in self.searches or search_string is None:
raise Http404
- search_instance = self.get_search_instance(slug, search_string)
+ search_instance = get_search_instance(slug, search_string)
return HttpResponse(json.dumps({
+ 'search': search_instance.slug,
'results': [result.get_context() for result in search_instance.results],
- 'rendered': [result.render() for result in search_instance.results],
- 'hasMoreResults': search.has_more_results(),
- 'moreResultsURL': (u"?%s" % search.more_results_querydict.urlencode()) if search.more_results_querydict else None,
+ 'hasMoreResults': search_instance.has_more_results,
+ 'moreResultsURL': search_instance.more_results_url,
}), mimetype="application/json")
\ No newline at end of file
#encoding: utf-8
import datetime
+from hashlib import sha1
from django.conf import settings
from django.contrib.sites.models import Site
from django.utils.http import urlquote_plus
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
-from django.template import loader, Context, Template
+from django.template import loader, Context, Template, TemplateDoesNotExist
from philo.contrib.sobol.utils import make_tracking_querydict, RegistryIterator
__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
+ instance = search(search_arg)
+ instance.slug = slug
+ return instance
+
+
+
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.
return self.search.get_result_title(self.result)
def get_url(self):
- """Returns the url of the result or an empty string by calling :meth:`BaseSearch.get_result_querydict` on the raw result and then encoding the querydict returned."""
- qd = self.search.get_result_querydict(self.result)
- if qd is None:
- return ""
- return "?%s" % qd.urlencode()
+ """Returns the url of the result or ``None`` by calling :meth:`BaseSearch.get_result_url` on the raw result. This url will contain a querystring which, if used, will track a :class:`.Click` for the actual url."""
+ return self.search.get_result_url(self.result)
+
+ def get_actual_url(self):
+ """Returns the actual url of the result by calling :meth:`BaseSearch.get_actual_result_url` on the raw result."""
+ return self.search.get_actual_result_url(self.result)
+
+ def get_content(self):
+ """Returns the content of the result by calling :meth:`BaseSearch.get_result_content` on the raw result."""
+ return self.search.get_result_content(self.result)
def get_template(self):
- """Returns the template for the result by calling :meth:`BaseSearch.get_result_template` on the raw result."""
+ """Returns the template which will be used to render the :class:`Result` by calling :meth:`BaseSearch.get_result_template` on the raw result."""
return self.search.get_result_template(self.result)
- def get_extra_context(self):
- """Returns any extra context for the result by calling :meth:`BaseSearch.get_result_extra_context` on the raw result."""
- return self.search.get_result_extra_context(self.result)
-
def get_context(self):
"""
- Returns the context dictionary for the result. This is used both in rendering the result and in the AJAX return value for :meth:`.SearchView.ajax_api_view`. The context will contain everything from :meth:`get_extra_context` as well as the following keys:
+ Returns the context dictionary for the result. This is used both in rendering the result and in the AJAX return value for :meth:`.SearchView.ajax_api_view`. The context will contain the following keys:
title
The result of calling :meth:`get_title`
url
The result of calling :meth:`get_url`
- result
- The raw result which the :class:`Result` was instantiated with.
+ content
+ The result of calling :meth:`get_content`
"""
- context = self.get_extra_context()
- context.update({
- 'title': self.get_title(),
- 'url': self.get_url(),
- 'result': self.result
- })
- return context
+ if not hasattr(self, '_context'):
+ self._context = {
+ 'title': self.get_title(),
+ 'url': self.get_url(),
+ 'actual_url': self.get_actual_url(),
+ 'content': self.get_content()
+ }
+ return self._context
def render(self):
"""Returns the template from :meth:`get_template` rendered with the context from :meth:`get_context`."""
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. If this is ``None``, then the framework will try ``sobol/search/<slug>/result.html`` and ``sobol/search/result.html``.
+ result_template = None
+ #: The path to the template which will be used to generate the title of the :class:`Result`\ s for this search. If this is ``None``, then the framework will try ``sobol/search/<slug>/title.html`` and ``sobol/search/title.html``.
+ title_template = None
+ #: The path to the template which will be used to generate the content of the :class:`Result`\ s for this search. If this is ``None``, then the framework will try ``sobol/search/<slug>/content.html`` and ``sobol/search/content.html``.
+ content_template = None
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:
+ for result in results:
+ result.get_context()
+ key = _make_cache_key(self, self.search_arg)
+ cache.set(key, self, self._cache_timeout)
return self._results
"""Returns an iterable of up to ``limit`` results. The :meth:`get_result_title`, :meth:`get_result_url`, :meth:`get_result_template`, and :meth:`get_result_extra_context` methods will be used to interpret the individual items that this function returns, so the result can be an object with attributes as easily as a dictionary with keys. However, keep in mind that the raw results will be stored with django's caching mechanisms and will be converted to JSON."""
raise NotImplementedError
- def get_result_title(self, result):
- """Returns the title of the ``result``. Must be implemented by subclasses."""
- raise NotImplementedError
-
- def get_result_url(self, result):
+ def get_actual_result_url(self, result):
"""Returns the actual URL for the ``result`` or ``None`` if there is no URL. Must be implemented by subclasses."""
raise NotImplementedError
def get_result_querydict(self, result):
"""Returns a querydict for tracking selection of the result, or ``None`` if there is no URL for the result."""
- url = self.get_result_url(result)
+ url = self.get_actual_result_url(result)
if url is None:
return None
return make_tracking_querydict(self.search_arg, url)
+ def get_result_url(self, result):
+ """Returns ``None`` or a url which, when accessed, will register a :class:`.Click` for that url."""
+ qd = self.get_result_querydict(result)
+ if qd is None:
+ return None
+ return "?%s" % qd.urlencode()
+
+ def get_result_title(self, result):
+ """Returns the title of the ``result``. By default, renders ``sobol/search/<slug>/title.html`` or ``sobol/search/title.html`` with the result in the context. This can be overridden by setting :attr:`title_template` or simply overriding :meth:`get_result_title`. If no template can be found, this will raise :exc:`TemplateDoesNotExist`."""
+ return loader.render_to_string(self.title_template or [
+ 'sobol/search/%s/title.html' % self.slug,
+ 'sobol/search/title.html'
+ ], {'result': result})
+
+ def get_result_content(self, result):
+ """Returns the content for the ``result``. By default, renders ``sobol/search/<slug>/content.html`` or ``sobol/search/content.html`` with the result in the context. This can be overridden by setting :attr:`content_template` or simply overriding :meth:`get_result_content`. If no template is found, this will return an empty string."""
+ try:
+ return loader.render_to_string(self.content_template or [
+ 'sobol/search/%s/content.html' % self.slug,
+ 'sobol/search/content.html'
+ ], {'result': result})
+ except TemplateDoesNotExist:
+ return ""
+
def get_result_template(self, result):
- """Returns the template to be used for rendering the ``result``."""
- if hasattr(self, 'result_template'):
+ """Returns the template to be used for rendering the ``result``. For a search with slug ``google``, this would first try ``sobol/search/google/result.html``, then fall back on ``sobol/search/result.html``. Subclasses can override this by setting :attr:`result_template` to the path of another template."""
+ if 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
-
- def get_result_extra_context(self, result):
- """Returns any extra context to be used when rendering the ``result``."""
- return {}
+ return loader.select_template([
+ 'sobol/search/%s/result.html' % self.slug,
+ 'sobol/search/result.html'
+ ])
+ @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
+ def get_actual_more_results_url(self):
+ """Returns the actual url for more results. By default, simply returns ``None``."""
+ return None
- @property
- def more_results_querydict(self):
+ def get_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.get_actual_more_results_url()
+ if url:
+ return make_tracking_querydict(self.search_arg, url)
+ return None
+
+ @property
+ def more_results_url(self):
+ """Returns a URL which consists of a querystring which, when accessed, will log a :class:`.Click` for the actual URL."""
+ qd = self.get_more_results_querydict()
+ if qd is None:
+ return None
+ return "?%s" % qd.urlencode()
def __unicode__(self):
return self.verbose_name
def url(self):
"""The URL where the search gets its results. Composed from :attr:`search_url` and :attr:`query_format_str`."""
return self.search_url + self.query_format_str % urlquote_plus(self.search_arg)
-
- @property
- def more_results_url(self):
+
+ def get_actual_more_results_url(self):
return self.url
def parse_response(self, response, limit=None):
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)"
+ _more_results_url = None
@property
def query_format_str(self):
return True
return False
- @property
- def more_results_url(self):
+ def get_actual_more_results_url(self):
return self._more_results_url
+ def get_actual_result_url(self, result):
+ return result['unescapedUrl']
+
def get_result_title(self, result):
- return result['titleNoFormatting']
+ return mark_safe(result['titleNoFormatting'])
- def get_result_url(self, result):
- return result['unescapedUrl']
+ def get_result_content(self, result):
+ return mark_safe(result['content'])
registry.register(GoogleSearch)
--- /dev/null
+(function($){
+ var sobol = window.sobol = {};
+ sobol.favoredResults = []
+ sobol.favoredResultSearch = null;
+ sobol.search = function(){
+ var searches = sobol.searches = $('article.search');
+ if(sobol.favoredResults.length) sobol.favoredResultSearch = searches.eq(0);
+ for (var i=sobol.favoredResults.length ? 1 : 0;i<searches.length;i++) {
+ (function(){
+ var s = searches[i];
+ $.ajax({
+ url: s.getAttribute('data-url'),
+ dataType: 'json',
+ success: function(data){
+ sobol.onSuccess($(s), data);
+ },
+ error: function(data, textStatus, errorThrown){
+ sobol.onError($(s), textStatus, errorThrown);
+ }
+ });
+ }());
+ };
+ }
+ sobol.renderResult = function(result){
+ // Returns the result rendered as a string. Override this to provide custom rendering.
+ var url = result['url'],
+ title = result['title'],
+ content = result['content'],
+ rendered = '';
+
+ if(url){
+ rendered += "<dt><a href='" + url + "'>" + title + "</a></dt>";
+ } else {
+ rendered += "<dt>" + title + "</dt>";
+ }
+ if(content && content != ''){
+ rendered += "<dd>" + content + "</dd>"
+ }
+ return rendered
+ }
+ sobol.addFavoredResult = function(result) {
+ var dl = sobol.favoredResultSearch.find('dl');
+ if(!dl.length){
+ dl = $('<dl>');
+ dl.appendTo(sobol.favoredResultSearch);
+ sobol.favoredResultSearch.removeClass('loading');
+ }
+ dl[0].innerHTML += sobol.renderResult(result)
+ }
+ sobol.onSuccess = function(ele, data){
+ // hook for success!
+ ele.removeClass('loading');
+ if (data['results'].length) {
+ ele[0].innerHTML += "<dl>";
+ $.each(data['results'], function(i, v){
+ ele[0].innerHTML += sobol.renderResult(v);
+ })
+ ele[0].innerHTML += "</dl>";
+ if(data['hasMoreResults'] && data['moreResultsURL']) ele[0].innerHTML += "<footer><p><a href='" + data['moreResultsURL'] + "'>See more results</a></p></footer>";
+ } else {
+ ele.addClass('empty');
+ ele[0].innerHTML += "<p>No results found.</p>";
+ ele.slideUp();
+ }
+ if (sobol.favoredResultSearch){
+ for (var i=0;i<data['results'].length;i++){
+ var r = data['results'][i];
+ if ($.inArray(r['actual_url'], sobol.favoredResults) != -1){
+ sobol.addFavoredResult(r);
+ }
+ }
+ }
+ };
+ sobol.onError = function(ele, textStatus, errorThrown){
+ // Hook for error...
+ ele.removeClass('loading');
+ text = errorThrown ? errorThrown : textStatus ? textStatus : "Error occurred.";
+ ele[0].innerHTML += "<p>" + text + "</p>";
+ };
+ $(sobol.search);
+}(jQuery));
\ No newline at end of file
--- /dev/null
+{% extends 'admin/change_form.html' %}
+{% load i18n %}
+
+{% block javascripts %}{% endblock %}
+{% block object-tools %}{% endblock %}
+{% block title %}Results for "{{ original.string }}" | {% trans 'Django site admin' %}{% endblock %}
+{% block content_title %}<h1>Results for "{{ original.string }}"</h1>{% endblock %}
+{% block extrastyle %}
+ <style type="text/css">
+ .favored td{
+ font-weight:bold;
+ }
+ #changelist table{
+ width:100%;
+ }
+ </style>
+{% endblock %}
+
+{% block content %}
+ <div class="module" id="changelist">
+ <table>
+ <thead>
+ <tr>
+ <th>Weight</th>
+ <th>URL</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for result in original.get_weighted_results %}
+ <tr class="{% cycle 'row1' 'row2' %}{% if result in original.get_favored_results %} favored{% endif %}">
+ <td>{{ result.weight }}</td>
+ <td>{{ result.url }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ {% block submit_row %}
+ <div class="submit-row">
+ {% if not is_popup and has_delete_permission %}{% if change or show_delete %}<p class="deletelink-box"><a href="delete/" class="deletelink">{% trans "Delete" %}</a></p>{% endif %}{% endif %}
+ </div>
+ {% endblock %}
+{% endblock %}
\ No newline at end of file
--- /dev/null
+{% extends 'admin/change_list.html' %}
+
+{% block object-tools %}{% endblock %}
\ No newline at end of file
--- /dev/null
+{% extends 'admin/sobol/search/change_form.html' %}
+{% load i18n %}
+
+{% block extrastyle %}
+ <style type="text/css">
+ .favored td{
+ font-weight:bold;
+ }
+ #changelist{
+ border:none;
+ background:none;
+ }
+ thead th{color:#444;font-weight:bold;}
+ </style>
+{% endblock %}
+
+{% block submit_row %}
+ <div class="module footer">
+ <ul class="submit-row">
+ {% if not is_popup and has_delete_permission %}{% if change or show_delete %}<li class="left delete-link-container"><a href="delete/" class="delete-link">{% trans "Delete" %}</a></li>{% endif %}{% endif %}
+ </ul>
+ </div>
+{% endblock %}
\ No newline at end of file
+++ /dev/null
-{% extends "admin/base_site.html" %}
-
-<!-- LOADING -->
-{% load i18n %}
-
-<!-- EXTRASTYLES -->
-{% block extrastyle %}<style type="text/css">.favored{font-weight:bold;}</style>{% endblock %}
-
-<!-- BREADCRUMBS -->
-{% block breadcrumbs %}
- <div id="breadcrumbs">
- {% if queryset|length > 1 %}
- <a href="../../">{% trans "Home" %}</a> ›
- <a href="../">{{ app_label|capfirst }}</a> ›
- <a href="./">{{ opts.verbose_name_plural|capfirst }}</a> ›
- {% trans 'Search results for multiple objects' %}
- {% else %}
- <a href="../../../../">{% trans "Home" %}</a> ›
- <a href="../../../">{{ app_label|capfirst }}</a> ›
- <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> ›
- <a href="../">{{ queryset|first|truncatewords:"18" }}</a> ›
- {% trans 'Results' %}
- {% endif %}
- </div>
-{% endblock %}
-
-<!-- CONTENT -->
-{% block content %}
- <div class="container-grid delete-confirmation">
- {% for search in queryset %}
- <div class="group tabular">
- <h2>{{ search_string }}</h2>
- <div class="module table">
- <div class="module thead">
- <div class="tr">
- <div class="th">Weight</div>
- <div class="th">URL</div>
- </div>
- </div>
- <div class="module tbody">
- {% for result in search.get_weighted_results %}
- <div class="tr{% if result in search.get_favored_results %} favored{% endif %}">
- <div class="td">{{ result.weight }}</div>
- <div class="td">{{ result.url }}</div>
- </div>
- {% endfor %}
- </div>
- </div>
- </div>
- {% endfor %}
- </div>
-{% endblock %}
\ No newline at end of file
+++ /dev/null
-{% extends "admin/base_site.html" %}
-{% load i18n %}
-
-{% block extrastyle %}<style type="text/css">.favored{font-weight:bold;}</style>{% endblock %}
-
-{% block breadcrumbs %}
-<div class="breadcrumbs">
- {% if queryset|length > 1 %}
- <a href="../../">{% trans "Home" %}</a> ›
- <a href="../">{{ app_label|capfirst }}</a> ›
- <a href="./">{{ opts.verbose_name_plural|capfirst }}</a> ›
- {% trans 'Search results for multiple objects' %}
- {% else %}
- <a href="../../../../">{% trans "Home" %}</a> ›
- <a href="../../../">{{ app_label|capfirst }}</a> ›
- <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> ›
- <a href="../">{{ queryset|first|truncatewords:"18" }}</a> ›
- {% trans 'Results' %}
- {% endif %}
-</div>
-{% endblock %}
-
-
-{% block content %}
- {% for search in queryset %}
- <fieldset class="module">
- <h2>{{ search.string }}</h2>
- <table>
- <thead>
- <tr>
- <th>Weight</th>
- <th>URL</th>
- </tr>
- </thead>
- <tbody>
- {% for result in search.get_weighted_results %}
- <tr{% if result in search.favored_results %} class="favored"{% endif %}>
- <td>{{ result.weight }}</td>
- <td>{{ result.url }}</td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </fieldset>
- {% endfor %}
-{% endblock %}
\ No newline at end of file
+++ /dev/null
-<article>
- <h1><a href="{{ url }}">{{ title|safe }}</a></h1>
- <p>{{ content|safe }}</p>
-</article>
\ No newline at end of file
--- /dev/null
+{% with node.view.enable_ajax_api as ajax %}
+{% if ajax %}
+ {% if not suppress_scripts %}<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script><script type="text/javascript" src="{{ STATIC_URL }}sobol/ajax_search.js"></script>{% endif %}
+ <script type="text/javascript">
+ (function($){
+ var sobol = window.sobol;
+ sobol.favoredResults = [{% for r in favored_results %}"{{ r }}"{% if not forloop.last %}, {% endif %}{% endfor %}];
+ }(jQuery));
+ </script>
+{% endif %}
+{% if favored_results %}
+ <article class="search favored{% if ajax %} loading{% endif %}">
+ <header>
+ <h1>Favored results</h1>
+ </header>
+ {% if not ajax %}
+ <dl>
+ {% for search in searches %}
+ {% for result in search.results %}
+ {% if result.get_actual_url in favored_results %}
+ {{ result }}
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+ {% if search.get_actual_more_results_url in favored_results %}
+ <dt><a href="{{ search.more_results_url }}">More results for {{ search }}</a></dt>
+ {% endif %}
+ </dl>
+ {% endif %}
+ </article>
+{% endif %}
+{% for search in searches %}
+<article {% if ajax %}class="search loading {{ search.slug }}" data-url="{{ search.ajax_api_url }}"{% else %}class="search {{ search.slug }}{% if not search.results %} empty{% endif %}"{% endif %}>
+ <header>
+ <a name='{{ search.slug }}'></a>
+ <h1>{{ search }}</h1>
+ </header>
+ {% if not ajax %}
+ {% if search.results %}
+ <dl>
+ {% for result in search.results %}
+ {{ result }}
+ {% endfor %}
+ </dl>
+ {% if search.has_more_results and search.more_results_url %}
+ <footer>
+ <p><a href="{{ search.more_results_url }}">See more results</a></p>
+ </footer>
+ {% endif %}
+ {% else %}
+ <p>No results found.</p>
+ {% endif %}
+ {% endif %}
+</article>
+{% endfor %}
+{% endwith %}
\ No newline at end of file
--- /dev/null
+{{ result.content|truncatewords_html:20 }}
\ No newline at end of file
--- /dev/null
+<dt>{% if url %}<a href="{{ url }}">{% endif %}{{ title }}{% if url %}</a>{% endif %}</dt>
+{% if content %}<dd>{{ content }}</dd>{% endif %}
\ No newline at end of file