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],
                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
                        '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.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
 
 
 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):
                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)
        
                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_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):
                """
        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`
                
                title
                        The result of calling :meth:`get_title`
@@ -166,13 +166,14 @@ class Result(object):
                        The result of calling :meth:`get_content`
                
                """
                        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`."""
        
        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
        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
        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 __init__(self, search_arg):
                self.search_arg = search_arg
@@ -232,6 +237,8 @@ class BaseSearch(object):
                        self._results = results
                        
                        if USE_CACHE:
                        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)
                
                                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
        
                """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."""
        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)
                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()
        
                        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."""
        
        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_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']
        
                return result['unescapedUrl']
        
+       def get_result_title(self, result):
+               return mark_safe(result['titleNoFormatting'])
+       
        def get_result_content(self, result):
        def get_result_content(self, result):
-               return result['content']
+               return mark_safe(result['content'])
 
 
 registry.register(GoogleSearch)
 
 
 registry.register(GoogleSearch)
index dc93da1..b2ef413 100644 (file)
@@ -1,8 +1,11 @@
 (function($){
        var sobol = window.sobol = {};
 (function($){
        var sobol = window.sobol = {};
+       sobol.favoredResults = []
+       sobol.favoredResultSearch = null;
        sobol.search = function(){
                var searches = sobol.searches = $('article.search');
        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({
                        (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!
        sobol.onSuccess = function(ele, data){
                // hook for success!
-               ele.removeClass('loading')
+               ele.removeClass('loading');
                if (data['results'].length) {
                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(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...
        };
        sobol.onError = function(ele, textStatus, errorThrown){
                // Hook for error...
index a3d8108..99db761 100644 (file)
@@ -8,6 +8,27 @@
                }(jQuery));
        </script>
 {% endif %}
                }(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>
 {% 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>
                        </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 %}
                        </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