On Tue, 22 Nov 2005 07:09:06 -0800 [EMAIL PROTECTED] wrote:

 
> What is the Django Way (tm) to paginate complex queries with
> user-controlled parameters?  That is, if I want to paginate the
> results of a foos.get_list({'lots': 'of_complex', ['junk': 'in
> here']}) where the query terms are submitted via form, how should I
> go about that while maintaining the user-posted values from page to
> page?

For propagating values in forms, I have a custom template tag to do
it.  However, it requires that you put the request object in the
context object for your template.  In my project I'm using a
'pagevars' object to hold this kind of slightly random thing (i.e.
stuff that isn't particularly related to the view), so all my
custom template tags can expect to find pagevars.request in the context.

The code looks something like this:

-----
from django.core import template
from django.utils import html

class ForwardQueryParamNode(template.Node):
    def __init__(self, param_name):
        self.param_name = param_name

    def render(self, context):
        request = context['pagevars'].request
        return '<div><input type="hidden" name="' + self.param_name + \
            '" value="' + html.escape(request.GET.get(self.param_name, '')) + \
            '" /></div>'
        
# forward_query_param - turn a param in query string into a hidden
# input field.  Needs to be able to get the request object from the context
def do_forward_query_param(parser, token):
    """
    Turns a parameter in a query string into a hidden input field,
    allowing it to be 'forwarded' as part of the next request in
    a form submission.  It requires one argument (the name of the parameter),
    and also requires that the request object be in the context as 
    by putting the standard 'pagevars' object in the context.
    """
    try:
        tag_name, param_name = token.contents.split(None, 1)
    except ValueError:
        raise template.TemplateSyntaxError, \
            "forward_query_param tag requires an argument"
    param_name = param_name.strip('"')
    return ForwardQueryParamNode(param_name)

------
And in the template - inside a <form> put this:

{% forward_query_param "order" %}

I also have a 'paging_control' and a 'sorting_control' which have similar 
requirements, and automatically 'forward' or adjust all query string 
parameters.  I've attached them to this e-mail (along with an example
template and view) in case you find them useful.  They will need 
modification to work outside my app, but I have re-used them several
times already inside my app and found they work quite well.

This is not an official Django method at all, but it works for me.  There
must be something similar to this in the admin, but last time I looked
I was unable to extract and re-use it easily.  The new admin branch might
be different.

It does make me think that if we had a standard for putting the request
object into the context it would make template tags like this more
re-usable.

Finally, looking at my code, I think the <input> controls in the template
could be refactored much more nicely into their own custom template tags. 

Luke


-- 
"Outside of a dog, a book is a man's best friend... inside of a dog, 
it's too dark to read."

Luke Plant || L.Plant.98 (at) cantab.net || http://lukeplant.me.uk/
from django.views.generic import list_detail
from django.utils.httpwrappers import HttpResponseRedirect
from django.core.exceptions import Http404
from django.core.extensions import render_to_response

from django.models.members import members
from django.models.members.members import MemberDoesNotExist
from cciw.apps.cciw.common import *
from django.core.extensions import DjangoContext
from datetime import datetime, timedelta

def index(request):
    lookup_args = {'dummy_member__exact' : 'False', 'hidden__exact': 'False'} 
    if (request.GET.has_key('online')):
        lookup_args['last_seen__gte'] = datetime.now() - timedelta(minutes=3)
    
    extra_context = standard_extra_context(request, title='Members')
    order_option_to_lookup_arg(
        {'adj': ('date_joined',),
        'ddj': ('-date_joined',),
        'aun': ('user_name',),
        'dun': ('-user_name',),
        'arn': ('real_name',),
        'drn': ('-real_name',),
        'als': ('last_seen',),
        'dls': ('-last_seen',)},
        lookup_args, request, ('user_name',))
    extra_context['default_order'] = 'aun'
    
    try:
        search = '%' + request['search'] + '%'
        lookup_args['where'] = ["(user_name LIKE %s OR real_name LIKE %s)"]
        lookup_args['params'] = [search, search]
    except KeyError:
        pass
        
    return list_detail.object_list(request, 'members', 'members', 
        extra_context = extra_context, 
        template_name = 'cciw/members/index',
        paginate_by=50, extra_lookup_kwargs = lookup_args,
        allow_empty = True)
{% extends "cciw/standard" %} {% load view_extras %} {% load forums %} {% block content %}


{% forward_query_param "order" %} {% if object_list %}
{% paging_control %}
{% for member in object_list %} {% endfor %}
Icon Member name {% sorting_control "aun" "dun" "Sort by user name ascending" "Sort by user name descending" %} 'Real' name {% sorting_control "arn" "drn" "Sort by real name ascending" "Sort by real name descending" %} Comments Joined {% sorting_control "adj" "ddj" "Sort by 'date joined' ascending" "Sort by 'date joined' descending" %} Last seen {% sorting_control "als" "dls" "Sort by 'last seen' ascending" "Sort by 'last seen' descending" %}
{% if member.icon %} {{ member.icon_image }} {% endif %} {{ member.get_link }} {{ member.real_name|escape }} {{ member.comments|bb2html }} {% if member.date_joined %} {{ member.date_joined|date:"d M Y" }} {% endif %} {% if member.last_seen %} {{ member.last_seen|date:"d M Y h:i" }} {% endif %}
{% paging_control %}
{% else %}

No members found.

{% endif %} {% endblock %}
from django.core import template
from django.utils import html
from cciw.apps.cciw.settings import *
from cciw.apps.cciw.utils import *

def page_link(request, page_number, fragment = ''):
    """Constructs a link to a specific page using the request.  Returns HTML escaped value"""
    return html.escape(modified_query_string(request, {'page': str(page_number)}, fragment))

class PagingControlNode(template.Node):
    def __init__(self, fragment = ''):
        self.fragment = fragment
        
    def render(self, context):
        # context has been populated by
        # generic view paging mechanism
        cur_page = int(context['page'])-1
        total_pages = int(context['pages'])
        request = context['pagevars'].request
        output = []
        if (total_pages > 1):
            output.append("&mdash; Page %d  of %d &mdash;&nbsp;&nbsp; " %
                (cur_page + 1, total_pages))
            
            for i in range(0, total_pages):
                if (i > 0):
                    output.append(" | ")
                    
                if i == cur_page:
                    output.append('<span class="pagingLinkCurrent">'
                        + str(i+1) + '</span>')
                else:
                    output.append(
                        '<a title="%(title)s" class="pagingLink" href="%(href)s">%(pagenumber)d</a>' % \
                        { 'title': 'Page ' + str(i+1),
                          'href': page_link(request, i, self.fragment),
                          'pagenumber': i+1 })
            output.append(" | ")
            if cur_page > 0:
                output.append(
                    '<a class="pagingLink" title="Previous page" href="%s">&laquo;</a>' % \
                    page_link(request, cur_page - 1, self.fragment))
            else:
                output.append('<span class="pagingLinkCurrent">&laquo;</span>')
            output.append("&nbsp;")
            if cur_page < total_pages - 1:
                output.append(
                    '<a class="pagingLink" title="Next page" href="%s">&raquo;</a>' % \
                    page_link(request, cur_page + 1, self.fragment))
            else:
                output.append('<span class="pagingLinkCurrent">&raquo;</span>')
        return ''.join(output)
        

def do_paging_control(parser, token):
    """
    Creates a list of links to the pages of a generic view.
    The paging  control requires that the request object be in the context as 
    by putting the standard 'pagevars' object in the context.
    
    An optional parameter can be used which contains the fragment to use for 
    paging links, which allows paging to skip any initial common content on the page.
    
    Usage::

        {% paging_control %}

"""
    parts = token.contents.split(None, 1)
    if len(parts) > 1:
        return PagingControlNode(fragment = parts[1])
    else:
        return PagingControlNode()

class SortingControlNode(template.Node):
    def __init__(self, ascending_param, descending_param, 
                 ascending_title, descending_title):
        self.ascending_param = ascending_param
        self.descending_param = descending_param
        self.ascending_title = ascending_title
        self.descending_title = descending_title
    
    def render(self, context):
        request = context['pagevars'].request
        output = '<span class="sortingControl">'
        current_order = request.GET.get('order','')
        if current_order == '':
            try:
                current_order = context['default_order']
            except KeyError:
                current_order = ''
        
        if current_order == self.ascending_param:
            output += '<a title="' + self.descending_title +'" href="' +  \
                html.escape(modified_query_string(request, {'order': self.descending_param})) \
                + '"><img class="sortAscActive" src="' + CCIW_MEDIA_ROOT + \
                'images/arrow-up.gif" alt="Sorted ascending" /></a>' 
        elif current_order == self.descending_param:
            output += '<a title="' + self.ascending_title +'" href="' + \
                html.escape(modified_query_string(request, {'order': self.ascending_param})) \
                + '"><img class="sortDescActive" src="' + CCIW_MEDIA_ROOT + \
                'images/arrow-down.gif" alt="Sorted descending" /></a>'
        else:
            # query string resets page to zero if we use a new type of sorting
            output += '<a title="' + self.ascending_title +'" href="' + \
                html.escape(modified_query_string(request, 
                                                 {'order': self.ascending_param, 'page': 0})) \
                + '"><img class="sortAsc" src="' + CCIW_MEDIA_ROOT + \
                'images/arrow-up.gif" alt="Sort ascending" /></a>'
        
        output += '</span>'
        return output
        

def do_sorting_control(parser, token):
    """
    Creates a pair of links that are used for sorting a list on a field.
    Four parameters are accepted, which must be the query string parameter values that 
    should be used for ascending and descending sorts on a field, and the corresponding
    title text for those links (in that order). All arguments must be quoted with '"'
    
    The query string parameter name is always 'order', and the view function
    will need to check that parameter and adjust accordingly.  The view function
    should also add 'default_order' to the context, which allows the 
    sorting control to determine what the current sort order is if no
    'order' parameter exists in the query string.
    
    The sorting control requires that the request object be in the context as 
    by putting the standard 'pagevars' object in the context. It is
    used for various things, including passing on other query string parameters
    in the generated URLs.
    
    Usage::
    
        {% sorting_control "ad" "dd" "Sort date ascending" "Sort date descending" %}
        
    """
    bits = token.contents.split('"')
    if len(bits) != 9:
        raise template.TemplateSyntaxError, "sorting_control tag requires 4 quoted arguments"
    return SortingControlNode(bits[1].strip('"'), bits[3].strip('"'),
                              bits[5].strip('"'), bits[7].strip('"'),)

class ForwardQueryParamNode(template.Node):
    def __init__(self, param_name):
        self.param_name = param_name

    def render(self, context):
        # requires the standard extra context
        request = context['pagevars'].request
        return '<div><input type="hidden" name="' + self.param_name + \
            '" value="' + html.escape(request.GET.get(self.param_name, '')) + \
            '" /></div>'
        
# forward_query_param - turn a param in query string into a hidden
# input field.  Needs to be able to get the request object from the context
def do_forward_query_param(parser, token):
    """
    Turns a parameter in a query string into a hidden input field,
    allowing it to be 'forwarded' as part of the next request in
    a form submission.  It requires one argument (the name of the parameter),
    and also requires that the request object be in the context as 
    by putting the standard 'pagevars' object in the context.
    """
    try:
        tag_name, param_name = token.contents.split(None, 1)
    except ValueError:
        raise template.TemplateSyntaxError, \
            "forward_query_param tag requires an argument"
    param_name = param_name.strip('"')
    return ForwardQueryParamNode(param_name)
    
template.register_tag('paging_control', do_paging_control)
template.register_tag('sorting_control', do_sorting_control)
template.register_tag('forward_query_param', do_forward_query_param)

Reply via email to