Refactored BaseSearch/Result to use templates to render the title and content entries...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Fri, 10 Jun 2011 17:24:42 +0000 (13:24 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Fri, 10 Jun 2011 17:24:42 +0000 (13:24 -0400)
philo/contrib/sobol/models.py
philo/contrib/sobol/search.py
philo/contrib/sobol/static/sobol/ajax_search.js
philo/contrib/sobol/templates/sobol/search/_list.html
philo/contrib/sobol/templates/sobol/search/content.html [new file with mode: 0644]
philo/contrib/sobol/templates/sobol/search/result.html

index c437d17..b35133e 100644 (file)
@@ -301,7 +301,6 @@ class SearchView(MultiView):
                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_instance.has_more_results,
                        'moreResultsURL': search_instance.more_results_url,
                }), mimetype="application/json")
\ No newline at end of file
index 693f879..5f5fddc 100644 (file)
@@ -10,7 +10,7 @@ from django.utils import simplejson as json
 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
 
@@ -139,24 +139,24 @@ class Result(object):
                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_url` on the raw result."""
+               """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_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_template(self):
                """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_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`
@@ -166,13 +166,14 @@ class Result(object):
                        The result of calling :meth:`get_content`
                
                """
-               context = self.get_extra_context()
-               context.update({
-                       'title': self.get_title(),
-                       'url': self.get_url(),
-                       'content': self.get_content()
-               })
-               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`."""
@@ -206,8 +207,12 @@ class BaseSearch(object):
        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".
+       #: 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
@@ -232,6 +237,8 @@ class BaseSearch(object):
                        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)
                
@@ -252,17 +259,13 @@ class BaseSearch(object):
                """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_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)
@@ -274,13 +277,22 @@ class BaseSearch(object):
                        return None
                return "?%s" % qd.urlencode()
        
-       def get_result_content(self, result):
-               """Returns the content for the ``result`` or ``None`` if there is no content. Must be implemented by subclasses."""
-               raise NotImplementedError
+       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_extra_context(self, result):
-               """Returns any extra context to be used when rendering the ``result``. Make sure that any extra context can be serialized as JSON."""
-               return {}
+       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``. 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."""
@@ -413,14 +425,14 @@ class GoogleSearch(JSONSearch):
        def get_actual_more_results_url(self):
                return self._more_results_url
        
-       def get_result_title(self, result):
-               return result['titleNoFormatting']
-       
-       def get_result_url(self, result):
+       def get_actual_result_url(self, result):
                return result['unescapedUrl']
        
+       def get_result_title(self, result):
+               return mark_safe(result['titleNoFormatting'])
+       
        def get_result_content(self, result):
-               return result['content']
+               return mark_safe(result['content'])
 
 
 registry.register(GoogleSearch)
index dc93da1..b2ef413 100644 (file)
@@ -1,8 +1,11 @@
 (function($){
        var sobol = window.sobol = {};
+       sobol.favoredResults = []
+       sobol.favoredResultSearch = null;
        sobol.search = function(){
                var searches = sobol.searches = $('article.search');
-               for (var i=0;i<searches.length;i++) {
+               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({
                        }());
                };
        }
+       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')
+               ele.removeClass('loading');
                if (data['results'].length) {
-                       ele[0].innerHTML += "<dl>" + data['rendered'].join("") + "</dl>";
+                       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...
index a3d8108..99db761 100644 (file)
@@ -8,6 +8,27 @@
                }(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>
@@ -23,7 +44,7 @@
                        </dl>
                        {% if search.has_more_results and search.more_results_url %}
                        <footer>
-                               <p><a href="?{{ search.more_results_querydict.urlencode }}">See more results</a></p>
+                               <p><a href="{{ search.more_results_url }}">See more results</a></p>
                        </footer>
                        {% endif %}
                {% else %}
diff --git a/philo/contrib/sobol/templates/sobol/search/content.html b/philo/contrib/sobol/templates/sobol/search/content.html
new file mode 100644 (file)
index 0000000..82088ec
--- /dev/null
@@ -0,0 +1 @@
+{{ result.content|truncatewords_html:20 }}
\ No newline at end of file
index fbd89c7..c5a906a 100644 (file)
@@ -1,2 +1,2 @@
-<dt>{% if url %}<a href="{{ url }}">{% endif %}{{ title|safe }}{% if url %}</a>{% endif %}</dt>
-{% if content %}<dd>{{ content|safe|truncatewords_html:20 }}</dd>{% endif %}
\ No newline at end of file
+<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