Login

Yet another query string template tag

Author:
atms
Posted:
April 12, 2011
Language:
Python
Version:
1.2
Score:
2 (after 2 ratings)

This one works works with or without query string dicts defined in the context. And it handles replacement, addition and removal of values for parameters with multiple values.

Usage:

{% url view %}{% query_string qs tag+tags month=m %}

where view, qs (dict), tags (list of strings) and m (number) are defined in the context. Full detail in the doc string.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import re
from django.template import Library, Node, TemplateSyntaxError
from django.http import QueryDict
from django.utils.encoding import smart_str

@register.tag
def query_string(parser, token):
    """
    Template tag for creating and modifying query strings.

    Syntax:
        {% query_string  [<base_querystring>] [modifier]* [as <var_name>] %}

        modifier is <name><op><value> where op in {=, +, -}

    Parameters:
        - base_querystring: literal query string, e.g. '?tag=python&tag=django&year=2011',
                            or context variable bound to either
                            - a literal query string,
                            - a python dict with potentially lists as values, or
                            - a django QueryDict object
                            May be '' or None or missing altogether.
        - modifiers may be repeated and have the form <name><op><value>.
                           They are processed in the order they appear.
                           name is taken as is for a parameter name.
                           op is one of {=, +, -}.
                           = replace all existing values of name with value(s)
                           + add value(s) to existing values for name
                           - remove value(s) from existing values if present
                           value is either a literal parameter value 
                             or a context variable. If it is a context variable
                             it may also be bound to a list.
        - as <var name>: bind result to context variable instead of injecting in output
                         (same as in url tag).

    Examples:
    1.  {% query_string  '?tag=a&m=1&m=3&tag=b' tag+'c' m=2 tag-'b' as myqs %}

        Result: myqs == '?m=2&tag=a&tag=c'

    2.  context = {'qs':   {'tag': ['a', 'b'], 'year': 2011, 'month': 2},
                   'tags': ['c', 'd'],
                   'm': 4,}

        {% query_string qs tag+tags month=m %}

        Result: '?tag=a&tag=b&tag=c&tag=d&year=2011&month=4
    """
    # matches 'tagname1+val1' or 'tagname1=val1' but not 'anyoldvalue'
    mod_re = re.compile(r"^(\w+)(=|\+|-)(.*)$")
    bits = token.split_contents()
    qdict = None
    mods = []
    asvar = None
    bits = bits[1:]
    if len(bits) >= 2 and bits[-2] == 'as':
        asvar = bits[-1]
        bits = bits[:-2]
    if len(bits) >= 1:
        first = bits[0]
        if not mod_re.match(first):
            qdict = parser.compile_filter(first)
            bits = bits[1:]
    for bit in bits:
        match = mod_re.match(bit)
        if not match:
            raise TemplateSyntaxError("Malformed arguments to query_string tag")
        name, op, value = match.groups()
        mods.append((name, op, parser.compile_filter(value)))
    return QueryStringNode(qdict, mods, asvar)

class QueryStringNode(Node):
    def __init__(self, qdict, mods, asvar):
        self.qdict = qdict
        self.mods = mods
        self.asvar = asvar
    def render(self, context):
        mods = [(smart_str(k, 'ascii'), op, v.resolve(context))
                for k, op, v in self.mods]
        if self.qdict:
            qdict = self.qdict.resolve(context)
        else:
            qdict = None
        # Internally work only with QueryDict
        qdict = self._get_initial_query_dict(qdict)
        #assert isinstance(qdict, QueryDict)
        for k, op, v in mods:
            qdict.setlist(k, self._process_list(qdict.getlist(k), op, v))                   
        qstring = qdict.urlencode()
        if qstring:
            qstring = '?' + qstring
        if self.asvar:
            context[self.asvar] = qstring
            return ''
        else:
            return qstring
    def _get_initial_query_dict(self, qdict):
        if not qdict:
            qdict = QueryDict(None, mutable=True)
        elif isinstance(qdict, QueryDict):
            qdict = qdict.copy()
        elif isinstance(qdict, basestring):
            if qdict.startswith('?'):
                qdict = qdict[1:]
            qdict = QueryDict(qdict, mutable=True)
        else:
            # Accept any old dict or list of pairs.
            try:
                pairs = qdict.items()
            except:
                pairs = qdict
            qdict = QueryDict(None, mutable=True)
            # Enter each pair into QueryDict object:
            try:
                for k, v in pairs:
                    # Convert values to unicode so that detecting 
                    # membership works for numbers.
                    if isinstance(v, (list, tuple)):
                        for e in v:
                            qdict.appendlist(k,unicode(e))
                    else:
                        qdict.appendlist(k, unicode(v))
            except:
                # Wrong data structure, qdict remains empty.
                pass
        return qdict
    def _process_list(self, current_list, op, val):
        if not val:
            if op == '=':
                return []
            else:
                return current_list
        # Deal with lists only.
        if not isinstance(val, (list, tuple)):
            val = [val]
        val = [unicode(v) for v in val]
       # Remove
        if op == '-':
            for v in val:
                while v in current_list:
                    current_list.remove(v)
        # Replace
        elif op == '=':
            current_list = val
        # Add
        elif op == '+':
            for v in val:
                current_list.append(v)
        return current_list

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 2 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 2 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 9 months, 2 weeks ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 10 months, 1 week ago
  5. Help text hyperlinks by sa2812 11 months ago

Comments

piquadrat (on May 2, 2011):

This is awesome! Thank you so much!

#

akaihola (on October 19, 2012):

There's one piece missing from the code. After the imports this is needed:

register = Library()

#

Please login first before commenting.