Clarified sobol Search framework - now automatically finds result templates at "sobol...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 9 Jun 2011 21:20:50 +0000 (17:20 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 9 Jun 2011 21:20:50 +0000 (17:20 -0400)
philo/contrib/sobol/models.py
philo/contrib/sobol/search.py
philo/contrib/sobol/static/sobol/ajax_search.js
philo/contrib/sobol/templates/admin/sobol/search/change_form.html [new file with mode: 0644]
philo/contrib/sobol/templates/admin/sobol/search/change_list.html [new file with mode: 0644]
philo/contrib/sobol/templates/admin/sobol/search/grappelli_results.html [deleted file]
philo/contrib/sobol/templates/sobol/search/_list.html
philo/contrib/sobol/templates/sobol/search/basesearch.html [deleted file]
philo/contrib/sobol/templates/sobol/search/googlesearch.html [deleted file]
philo/contrib/sobol/templates/sobol/search/result.html [new file with mode: 0644]

index 1bef3cd..c437d17 100644 (file)
@@ -79,6 +79,8 @@ class Search(models.Model):
                                        self._favored_results += subresults
                                else:
                                        break
+                       if len(self._favored_results) == len(results):
+                               self._favored_results = []
                return self._favored_results
        
        class Meta:
@@ -168,7 +170,7 @@ try:
 except ImportError:
        pass
 else:
-       add_introspection_rules([], ["^philo\.contrib\.shipherd\.models\.RegistryChoiceField"])
+       add_introspection_rules([], ["^philo\.contrib\.sobol\.models\.RegistryChoiceField"])
 
 
 class SearchView(MultiView):
@@ -255,8 +257,16 @@ class SearchView(MultiView):
                                        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()
                
@@ -267,8 +277,10 @@ class SearchView(MultiView):
        
        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
@@ -276,7 +288,7 @@ class SearchView(MultiView):
                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)
@@ -288,7 +300,8 @@ class SearchView(MultiView):
                
                return HttpResponse(json.dumps({
                        'search': search_instance.slug,
-                       'results': [result.render() for result in search_instance.results],
+                       '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': (u"?%s" % search_instance.more_results_querydict.urlencode()) if search_instance.more_results_querydict else None,
+                       'moreResultsURL': search_instance.more_results_url,
                }), mimetype="application/json")
\ No newline at end of file
index 2c31158..693f879 100644 (file)
@@ -139,20 +139,21 @@ 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_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 an empty string by calling :meth:`BaseSearch.get_result_url` on the raw result."""
+               return self.search.get_result_url(self.result)
        
-       def get_template(self):
-               """Returns the template for the result by calling :meth:`BaseSearch.get_result_template` on the raw result."""
-               return self.search.get_result_template(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:
@@ -161,15 +162,15 @@ class Result(object):
                        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
+                       'content': self.get_content()
                })
                return context
        
@@ -205,8 +206,8 @@ 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.
-       result_template = "sobol/search/basesearch.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
        
        def __init__(self, search_arg):
                self.search_arg = search_arg
@@ -255,7 +256,7 @@ class BaseSearch(object):
                """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
        
@@ -266,32 +267,54 @@ class BaseSearch(object):
                        return None
                return make_tracking_querydict(self.search_arg, url)
        
-       def get_result_template(self, result):
-               """Returns the template to be used for rendering the ``result``."""
-               return loader.get_template(self.result_template)
+       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_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_extra_context(self, result):
-               """Returns any extra context to be used when rendering the ``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_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."""
+               if self.result_template:
+                       return loader.get_template(self.result_template)
+               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. By default, simply returns ``None``."""
+       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."""
-               url = 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
 
@@ -325,9 +348,8 @@ class URLSearch(BaseSearch):
        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):
@@ -349,7 +371,6 @@ class GoogleSearch(JSONSearch):
        search_url = "http://ajax.googleapis.com/ajax/services/search/web"
        _cache_timeout = 60
        verbose_name = "Google search (current site)"
-       result_template = "sobol/search/googlesearch.html"
        _more_results_url = None
        
        @property
@@ -389,8 +410,7 @@ class GoogleSearch(JSONSearch):
                        return True
                return False
        
-       @property
-       def more_results_url(self):
+       def get_actual_more_results_url(self):
                return self._more_results_url
        
        def get_result_title(self, result):
@@ -398,6 +418,9 @@ class GoogleSearch(JSONSearch):
        
        def get_result_url(self, result):
                return result['unescapedUrl']
+       
+       def get_result_content(self, result):
+               return result['content']
 
 
 registry.register(GoogleSearch)
index 33fdf4f..dc93da1 100644 (file)
@@ -22,7 +22,7 @@
                // hook for success!
                ele.removeClass('loading')
                if (data['results'].length) {
-                       ele[0].innerHTML += "<dl>" + data['results'].join("") + "</dl>";
+                       ele[0].innerHTML += "<dl>" + data['rendered'].join("") + "</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');
diff --git a/philo/contrib/sobol/templates/admin/sobol/search/change_form.html b/philo/contrib/sobol/templates/admin/sobol/search/change_form.html
new file mode 100644 (file)
index 0000000..2761599
--- /dev/null
@@ -0,0 +1,44 @@
+{% 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{
+                       border:none;
+                       background:none;
+               }
+       </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>
+       <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
diff --git a/philo/contrib/sobol/templates/admin/sobol/search/change_list.html b/philo/contrib/sobol/templates/admin/sobol/search/change_list.html
new file mode 100644 (file)
index 0000000..9b01661
--- /dev/null
@@ -0,0 +1,3 @@
+{% extends 'admin/change_list.html' %}
+
+{% block object-tools %}{% endblock %}
\ No newline at end of file
diff --git a/philo/contrib/sobol/templates/admin/sobol/search/grappelli_results.html b/philo/contrib/sobol/templates/admin/sobol/search/grappelli_results.html
deleted file mode 100644 (file)
index f01eb88..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-{% 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> &rsaquo;
-               <a href="../">{{ app_label|capfirst }}</a> &rsaquo;
-               <a href="./">{{ opts.verbose_name_plural|capfirst }}</a> &rsaquo;
-               {% trans 'Search results for multiple objects' %}
-               {% else %}
-               <a href="../../../../">{% trans "Home" %}</a> &rsaquo;
-               <a href="../../../">{{ app_label|capfirst }}</a> &rsaquo;
-               <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> &rsaquo;
-               <a href="../">{{ queryset|first|truncatewords:"18" }}</a> &rsaquo;
-               {% 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
index 8fed939..a3d8108 100644 (file)
@@ -1,5 +1,13 @@
 {% with node.view.enable_ajax_api as ajax %}
-{% if ajax and 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 %}
+{% 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 %}
 {% 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>
@@ -10,7 +18,7 @@
                {% if search.results %}
                        <dl>
                        {% for result in search.results %}
-                       {{ result }}
+                               {{ result }}
                        {% endfor %}
                        </dl>
                        {% if search.has_more_results and search.more_results_url %}
diff --git a/philo/contrib/sobol/templates/sobol/search/basesearch.html b/philo/contrib/sobol/templates/sobol/search/basesearch.html
deleted file mode 100644 (file)
index e7661ac..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<dt>{% if url %}<a href='{{ url }}'>{% endif %}{{ title }}{% if url %}</a>{% endif %}</dt>
\ No newline at end of file
diff --git a/philo/contrib/sobol/templates/sobol/search/googlesearch.html b/philo/contrib/sobol/templates/sobol/search/googlesearch.html
deleted file mode 100644 (file)
index 8d23132..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-<dt><a href="{{ url }}">{{ title|safe }}</a></dt>
-<dd>{{ result.content|safe }}</dd>
\ No newline at end of file
diff --git a/philo/contrib/sobol/templates/sobol/search/result.html b/philo/contrib/sobol/templates/sobol/search/result.html
new file mode 100644 (file)
index 0000000..fbd89c7
--- /dev/null
@@ -0,0 +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