README more appealing (has feature list), added RequestConfig, added ability to speci...
authorBradley Ayers <bradley.ayers@gmail.com>
Sun, 17 Jul 2011 11:10:28 +0000 (21:10 +1000)
committerBradley Ayers <bradley.ayers@gmail.com>
Sun, 17 Jul 2011 11:10:28 +0000 (21:10 +1000)
15 files changed:
README.rst
django_tables2/__init__.py
django_tables2/config.py [new file with mode: 0644]
django_tables2/tables.py
django_tables2/templates/django_tables2/table.html
django_tables2/templatetags/django_tables2.py
docs/index.rst
example/app/models.py
example/app/views.py
setup.py
tests/__init__.py
tests/config.py [new file with mode: 0644]
tests/core.py
tests/models.py
tests/templates.py

index d75778733429d1bba42aef07dd557224167443fc..65244bf52be8f73d945e69eeeecfaca960bb3a77 100644 (file)
@@ -13,6 +13,18 @@ django-tables2 simplifies the task of turning sets of data into HTML tables. It
 has native support for pagination and sorting. It does for HTML tables what
 ``django.forms`` does for HTML forms.
 
+Its features include:
+
+- Any iterable can be a data-source, but special support for Django querysets
+  is included.
+- The builtin UI does not rely on JavaScript.
+- Support for automatic table generation based on a Django model.
+- Supports custom column functionality via subclassing.
+- Pagination.
+- Column based table sorting.
+- Template tag to enable trivial rendering to HTML.
+- Generic view mixin for use in Django 1.3.
+
 Creating a table is as simple as::
 
     import django_tables2 as tables
index 3437774d90a824ae240a75de800036e523efce65..5733c1d25eeaf4ef3e06129ee6e06bec9fdee9fe 100644 (file)
@@ -1,2 +1,3 @@
 from .tables import Table
 from .columns import *
+from .config import RequestConfig
diff --git a/django_tables2/config.py b/django_tables2/config.py
new file mode 100644 (file)
index 0000000..3ec178e
--- /dev/null
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+
+class RequestConfig(object):
+    """
+    A configurator that uses request data to setup a table.
+
+    :type paginate: ``dict`` or ``bool``
+    :param paginate: indicates whether to paginate, and if so, what default
+            values to use. If the value evaluates to ``False``, pagination
+            will be disabled. A ``dict`` can be used to specify default values
+            for the call to :meth:`.tables.Table.paginate` (e.g. to define a default
+            ``per_page`` value).
+
+    """
+    def __init__(self, request, paginate=True):
+        self.request = request
+        self.paginate = paginate
+
+    def configure(self, table):
+        """
+        Configure a table using information from the request.
+        """
+        GET = self.request.GET  # makes our lines shorter
+        table.order_by = GET.getlist(table.prefixed_order_by_field)
+        if self.paginate:
+            if hasattr(self.paginate, "items"):
+                kwargs = dict(self.paginate)
+            else:
+                kwargs = {}
+            # extract some options from the request
+            for x in ("page", "per_page"):
+                name = getattr(table, u"prefixed_%s_field" % x)
+                if name in self.request.GET:
+                    try:
+                        kwargs[x] = int(self.request.GET[name])
+                    except ValueError:
+                        pass
+            table.paginate(**kwargs)
index abe49de602f4462f9ab9f44d8056a26229f3b0b6..d5d8720cc0fc3bf5f2965992ebf0664d72331cb8 100644 (file)
@@ -183,7 +183,8 @@ class DeclarativeColumnsMetaclass(type):
 class TableOptions(object):
     """
     Extracts and exposes options for a :class:`.Table` from a ``class Meta``
-    when the table is defined.
+    when the table is defined. See ``Table`` for documentation on the impact of
+    variables in this class.
 
     :param options: options for a table
     :type options: :class:`Meta` on a :class:`.Table`
@@ -198,6 +199,10 @@ class TableOptions(object):
         if isinstance(order_by, basestring):
             order_by = (order_by, )
         self.order_by = OrderByTuple(order_by)
+        self.order_by_field = getattr(options, "order_by_field", "sort")
+        self.page_field = getattr(options, "page_field", "page")
+        self.per_page_field = getattr(options, "per_page_field", "per_page")
+        self.prefix = getattr(options, "prefix", "")
         self.sequence = Sequence(getattr(options, "sequence", ()))
         self.sortable = getattr(options, "sortable", True)
         self.model = getattr(options, "model", None)
@@ -207,19 +212,52 @@ class Table(StrAndUnicode):
     """
     A collection of columns, plus their associated data rows.
 
+    :type attrs: ``dict``
+    :param attrs: A mapping of attributes to values that will be added to the
+            HTML ``<table>`` tag.
+
     :type data:  ``list`` or ``QuerySet``
     :param data: The :term:`table data`.
 
+    :type exclude: *iterable*
+    :param exclude: A list of columns to be excluded from this table.
+
     :type order_by: ``None``, ``tuple`` or ``string``
     :param order_by: sort the table based on these columns prior to display.
-        (default :attr:`.Table.Meta.order_by`)
+            (default :attr:`.Table.Meta.order_by`)
+
+    :type order_by_field: ``string`` or ``None``
+    :param order_by_field: The name of the querystring field used to control
+            the table ordering.
+
+    :type page_field: ``string`` or ``None``
+    :param page_field: The name of the querystring field used to control which
+            page of the table is displayed (used when a table is paginated).
+
+    :type per_page_field: ``string`` or ``None``
+    :param per_page_field: The name of the querystring field used to control
+            how many records are displayed on each page of the table.
+
+    :type prefix: ``string``
+    :param prefix: A prefix used on querystring arguments to allow multiple
+            tables to be used on a single page, without having conflicts
+            between querystring arguments. Depending on how the table is
+            rendered, will determine how the prefix is used. For example ``{%
+            render_table %}`` uses ``<prefix>-<argument>``.
+
+    :type sequence: *iterable*
+    :param sequence: The sequence/order of columns the columns (from left to
+            right). Items in the sequence must be column names, or the
+            *remaining items* symbol marker ``"..."`` (string containing three
+            periods). If this marker is used, not all columns need to be
+            defined.
 
     :type sortable: ``bool``
     :param sortable: Enable/disable sorting on this table
 
     :type empty_text: ``string``
     :param empty_text: Empty text to render when the table has no data.
-        (default :attr:`.Table.Meta.empty_text`)
+            (default :attr:`.Table.Meta.empty_text`)
 
     The ``order_by`` argument is optional and allows the table's
     ``Meta.order_by`` option to be overridden. If the ``order_by is None``
@@ -244,13 +282,18 @@ class Table(StrAndUnicode):
     TableDataClass = TableData
 
     def __init__(self, data, order_by=None, sortable=None, empty_text=None,
-                 exclude=None, attrs=None, sequence=None):
+                 exclude=None, attrs=None, sequence=None, prefix=None,
+                 order_by_field=None, page_field=None, per_page_field=None):
         self._rows = BoundRows(self)
         self._columns = BoundColumns(self)
         self._data = self.TableDataClass(data=data, table=self)
         self.attrs = attrs
         self.empty_text = empty_text
         self.sortable = sortable
+        self.prefix = prefix
+        self.order_by_field = order_by_field
+        self.page_field = page_field
+        self.per_page_field = per_page_field
         # Make a copy so that modifying this will not touch the class
         # definition. Note that this is different from forms, where the
         # copy is made available in a ``fields`` attribute.
@@ -265,10 +308,52 @@ class Table(StrAndUnicode):
         else:
             self.order_by = order_by
 
+    def __unicode__(self):
+        return unicode(repr(self))
+
+    def as_html(self):
+        """
+        Render the table to a simple HTML table.
+
+        The rendered table won't include pagination or sorting, as those
+        features require a RequestContext. Use the ``render_table`` template
+        tag (requires ``{% load django_tables2 %}``) if you require this extra
+        functionality.
+        """
+        template = get_template('django_tables2/basic_table.html')
+        return template.render(Context({'table': self}))
+
+    @property
+    def attrs(self):
+        """
+        The attributes that should be applied to the ``<table>`` tag when
+        rendering HTML.
+
+        :rtype: :class:`~.utils.AttributeDict` object.
+        """
+        return self._attrs if self._attrs is not None else self._meta.attrs
+
+    @attrs.setter
+    def attrs(self, value):
+        self._attrs = value
+
+    @property
+    def columns(self):
+        return self._columns
+
     @property
     def data(self):
         return self._data
 
+    @property
+    def empty_text(self):
+        return (self._empty_text if self._empty_text is not None
+                                 else self._meta.empty_text)
+
+    @empty_text.setter
+    def empty_text(self, value):
+        self._empty_text = value
+
     @property
     def order_by(self):
         return self._order_by
@@ -294,6 +379,78 @@ class Table(StrAndUnicode):
         self._order_by = order_by
         self._data.order_by(order_by)
 
+    @property
+    def order_by_field(self):
+        return (self._order_by_field if self._order_by_field is not None
+                else self._meta.order_by_field)
+
+    @order_by_field.setter
+    def order_by_field(self, value):
+        self._order_by_field = value
+
+    @property
+    def page_field(self):
+        return (self._page_field if self._page_field is not None
+                else self._meta.page_field)
+
+    @page_field.setter
+    def page_field(self, value):
+        self._page_field = value
+
+    def paginate(self, klass=Paginator, per_page=25, page=1, *args, **kwargs):
+        """
+        Paginates the table using a paginator and creates a ``page`` property
+        containing information for the current page.
+
+        :type klass: Paginator ``class``
+        :param klass: a paginator class to paginate the results
+
+        :type per_page: ``int``
+        :param per_page: how many records are displayed on each page
+
+        :type page: ``int``
+        :param page: which page should be displayed.
+        """
+        self.paginator = klass(self.rows, per_page, *args, **kwargs)
+        try:
+            self.page = self.paginator.page(page)
+        except Exception as e:
+            raise Http404(str(e))
+
+    @property
+    def per_page_field(self):
+        return (self._per_page_field if self._per_page_field is not None
+                else self._meta.per_page_field)
+
+    @per_page_field.setter
+    def per_page_field(self, value):
+        self._per_page_field = value
+
+    @property
+    def prefix(self):
+        return (self._prefix if self._prefix is not None
+                else self._meta.prefix)
+
+    @prefix.setter
+    def prefix(self, value):
+        self._prefix = value
+
+    @property
+    def prefixed_order_by_field(self):
+        return u"%s%s" % (self.prefix, self.order_by_field)
+
+    @property
+    def prefixed_page_field(self):
+        return u"%s%s" % (self.prefix, self.page_field)
+
+    @property
+    def prefixed_per_page_field(self):
+        return u"%s%s" % (self.prefix, self.per_page_field)
+
+    @property
+    def rows(self):
+        return self._rows
+
     @property
     def sequence(self):
         return (self._sequence if self._sequence is not None
@@ -314,53 +471,3 @@ class Table(StrAndUnicode):
     @sortable.setter
     def sortable(self, value):
         self._sortable = value
-
-    @property
-    def empty_text(self):
-        return (self._empty_text if self._empty_text is not None
-                                 else self._meta.empty_text)
-
-    @empty_text.setter
-    def empty_text(self, value):
-        self._empty_text = value
-
-    @property
-    def rows(self):
-        return self._rows
-
-    @property
-    def columns(self):
-        return self._columns
-
-    def as_html(self):
-        """
-        Render the table to a simple HTML table.
-
-        The rendered table won't include pagination or sorting, as those
-        features require a RequestContext. Use the ``render_table`` template
-        tag (requires ``{% load django_tables2 %}``) if you require this extra
-        functionality.
-        """
-        template = get_template('django_tables2/basic_table.html')
-        return template.render(Context({'table': self}))
-
-    @property
-    def attrs(self):
-        """
-        The attributes that should be applied to the ``<table>`` tag when
-        rendering HTML.
-
-        :rtype: :class:`~.utils.AttributeDict` object.
-        """
-        return self._attrs if self._attrs is not None else self._meta.attrs
-
-    @attrs.setter
-    def attrs(self, value):
-        self._attrs = value
-
-    def paginate(self, klass=Paginator, per_page=25, page=1, *args, **kwargs):
-        self.paginator = klass(self.rows, per_page, *args, **kwargs)
-        try:
-            self.page = self.paginator.page(page)
-        except Exception as e:
-            raise Http404(str(e))
index d580a1a591eb7a8aba4b1b6d1a24248c7a229a93..3a9bac4207dac34d347aabd4870efbb9dcec972b 100644 (file)
@@ -9,7 +9,7 @@
         {% for column in table.columns %}
         {% if column.sortable %}
             {% with column.order_by as ob %}
-            <th class="{% spaceless %}{% if column.sortable %}sortable {% endif %}{% if ob %}{% if ob.is_descending %}desc{% else %}asc{% endif %}{% endif %}{% endspaceless %}"><a href="{% if ob %}{% set_url_param sort=ob.opposite %}{% else %}{% set_url_param sort=column.name %}{% endif %}">{{ column.header }}</a></th>
+            <th class="{% spaceless %}{% if column.sortable %}sortable {% endif %}{% if ob %}{% if ob.is_descending %}desc{% else %}asc{% endif %}{% endif %}{% endspaceless %}"><a href="{% querystring table.prefixed_order_by_field=ob.opposite|default:column.name %}">{{ column.header }}</a></th>
             {% endwith %}
         {% else %}
             <th>{{ column.header }}</th>
 {% if table.page %}
 <ul class="pagination">
     {% if table.page.has_previous %}
-    <li class="previous"><a href="{% set_url_param page=table.page.previous_page_number %}">Previous</a></li>
+    <li class="previous"><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">Previous</a></li>
     {% endif %}
     <li class="current">Page {{ table.page.number }} of {{ table.paginator.num_pages }}</li>
     {% if table.page.has_next %}
-    <li class="next"><a href="{% set_url_param page=table.page.next_page_number %}">Next</a></li>
+    <li class="next"><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">Next</a></li>
     {% endif %}
 </ul>
 </div>
index 3b1a6a5f73c7ac88ed0947747454c4c8fbce83a0..6107072120505fba4552c064a859df1640b72a7a 100644 (file)
@@ -1,4 +1,5 @@
 #! -*- coding: utf-8 -*-
+from __future__ import absolute_import
 """
 Allows setting/changing/removing of chosen url query string parameters, while
 maintaining any existing others.
@@ -12,19 +13,46 @@ Examples:
     {% set_url_param filter="books" page=1 %}
 
 """
-import urllib
+import re
 import tokenize
 import StringIO
 from django.conf import settings
 from django import template
+from django.template import TemplateSyntaxError, Context, Variable, Node
+from django.utils.datastructures import SortedDict
 from django.template.loader import get_template
 from django.utils.safestring import mark_safe
+from django.utils.http import urlencode
+import django_tables2 as tables
 
 
 register = template.Library()
+kwarg_re = re.compile(r"(?:(.+)=)?(.+)")
 
 
-class SetUrlParamNode(template.Node):
+def token_kwargs(bits, parser):
+    """
+    Based on Django's ``django.template.defaulttags.token_kwargs``, but with a
+    few changes:
+
+    - No legacy mode.
+    - Both keys and values are compiled as a filter
+
+    """
+    if not bits:
+        return {}
+    kwargs = SortedDict()
+    while bits:
+        match = kwarg_re.match(bits[0])
+        if not match or not match.group(1):
+            return kwargs
+        key, value = match.groups()
+        del bits[:1]
+        kwargs[parser.compile_filter(key)] = parser.compile_filter(value)
+    return kwargs
+
+
+class SetUrlParamNode(Node):
     def __init__(self, changes):
         self.changes = changes
 
@@ -32,9 +60,6 @@ class SetUrlParamNode(template.Node):
         request = context.get('request', None)
         if not request:
             return ""
-        # Note that we want params to **not** be a ``QueryDict`` (thus we
-        # don't use it's ``copy()`` method), as it would force all values
-        # to be unicode, and ``urllib.urlencode`` can't handle that.
         params = dict(request.GET)
         for key, newvalue in self.changes.items():
             newvalue = newvalue.resolve(context)
@@ -42,28 +67,22 @@ class SetUrlParamNode(template.Node):
                 params.pop(key, False)
             else:
                 params[key] = unicode(newvalue)
-
-        # ``urlencode`` chokes on unicode input, so convert everything to utf8.
-        # Note that if some query arguments passed to the site have their
-        # non-ascii characters screwed up when passed though this, it's most
-        # likely not our fault. Django (the ``QueryDict`` class to be exact)
-        # uses your projects DEFAULT_CHARSET to decode incoming query strings,
-        # whereas your browser might encode the url differently. For example,
-        # typing "Ƥ" in my German Firefox's (v2) address bar results in "%E4"
-        # being passed to the server (in iso-8859-1), but Django might expect
-        # utf-8, where Ć¤ would be "%C3%A4"
-        def mkstr(s):
-            if isinstance(s, list):
-                return map(mkstr, s)
-            else:
-                return s.encode('utf-8') if isinstance(s, unicode) else s
-        params = dict([(mkstr(k), mkstr(v)) for k, v in params.items()])
-        # done, return (string is already safe)
-        return '?' + urllib.urlencode(params, doseq=True)
+        return "?" + urlencode(params, doseq=True)
 
 
 @register.tag
 def set_url_param(parser, token):
+    """
+    Creates a URL (containing only the querystring [including "?"]) based on
+    the current URL, but updated with the provided keyword arguments.
+
+    Example::
+
+        {% set_url_param name="help" age=20 %}
+        ?name=help&age=20
+
+    **Deprecated** as of 0.7.0, use ``updateqs``.
+    """
     bits = token.contents.split()
     qschanges = {}
     for i in bits[1:]:
@@ -75,32 +94,73 @@ def set_url_param(parser, token):
             keys = list(tokenize.generate_tokens(a_line_iter))
             if keys[0][0] == tokenize.NAME:
                 # workaround bug #5270
-                b = (template.Variable(b) if b == '""' else
-                     parser.compile_filter(b))
+                b = Variable(b) if b == '""' else parser.compile_filter(b)
                 qschanges[str(a)] = b
             else:
                 raise ValueError
         except ValueError:
-            raise (template.TemplateSyntaxError,
-                   "Argument syntax wrong: should be key=value")
+            raise TemplateSyntaxError("Argument syntax wrong: should be"
+                                      "key=value")
     return SetUrlParamNode(qschanges)
 
 
-class RenderTableNode(template.Node):
-    def __init__(self, table_var_name):
-        self.table_var = template.Variable(table_var_name)
+class QuerystringNode(Node):
+    def __init__(self, params):
+        self.params = params
+
+    def render(self, context):
+        request = context.get('request', None)
+        if not request:
+            return ""
+        params = dict(request.GET)
+        for key, value in self.params.iteritems():
+            key = key.resolve(context)
+            value = value.resolve(context)
+            if key not in ("", None):
+                params[key] = value
+        return "?" + urlencode(params, doseq=True)
+
+
+# {% querystring "name"="abc" "age"=15 %}
+@register.tag
+def querystring(parser, token):
+    """
+    Creates a URL (containing only the querystring [including "?"]) derived
+    from the current URL's querystring, by updating it with the provided
+    keyword arguments.
+
+    Example (imagine URL is /abc/?gender=male&name=Brad::
+
+        {% querystring "name"="Ayers" "age"=20 %}
+        ?name=Ayers&gender=male&age=20
+    """
+    bits = token.split_contents()
+    tag = bits.pop(0)
+    try:
+        return QuerystringNode(token_kwargs(bits, parser))
+    finally:
+        # ``bits`` should now be empty, if this is not the case, it means there
+        # was some junk arguments that token_kwargs couldn't handle.
+        if bits:
+            raise TemplateSyntaxError("Malformed arguments to '%s'" % tag)
+
+
+class RenderTableNode(Node):
+    def __init__(self, table):
+        self.table = table
 
     def render(self, context):
         try:
-            # may raise VariableDoesNotExist
-            table = self.table_var.resolve(context)
+            table = self.table.resolve(context)
+            if not isinstance(table, tables.Table):
+                raise ValueError("Expected Table object, but didn't find one.")
             if "request" not in context:
-                raise AssertionError("{% render_table %} requires that the "
-                                     "template context contains the HttpRequest in"
-                                     " a 'request' variable, check your "
-                                     " TEMPLATE_CONTEXT_PROCESSORS setting.")
-            context = template.Context({"request": context["request"],
-                                        "table": table})
+                raise AssertionError(
+                        "{% render_table %} requires that the template context"
+                        " contains the HttpRequest in a 'request' variable, "
+                        "check your TEMPLATE_CONTEXT_PROCESSORS setting.")
+            context = Context({"request": context["request"], "table": table})
+            # HACK! :(
             try:
                 table.request = context["request"]
                 return get_template("django_tables2/table.html").render(context)
@@ -115,10 +175,7 @@ class RenderTableNode(template.Node):
 
 @register.tag
 def render_table(parser, token):
-    try:
-        _, table_var_name = token.contents.split()
-    except ValueError:
-        raise (template.TemplateSyntaxError,
-               u'%r tag requires a single argument'
-               % token.contents.split()[0])
-    return RenderTableNode(table_var_name)
+    bits = token.split_contents()
+    if len(bits) != 2:
+        raise TemplateSyntaxError("'%s' requires one argument." % bits[0])
+    return RenderTableNode(parser.compile_filter(bits[1]))
index 865ca60fa498b85293e4361eba5cb0368d94cdd8..86c3bc72b5953a0c548fcfe6e382cf4dec6241b0 100644 (file)
@@ -205,7 +205,7 @@ By passing in a value for ``order_by`` into the ``Table`` constructor, the
 
 This approach allows column sorting to be enabled for use with the ``{%
 render_table %}`` template tag. The template tag converts column headers into
-hyperlinks that add the querystring parameter ``sort`` to the current URL. This
+hyperlinks that add the querystring field ``sort`` to the current URL. This
 means your view will need to look something like:
 
 .. code-block:: python
@@ -216,6 +216,8 @@ means your view will need to look something like:
         return render_to_response('home.html', {'table': table},
                                   context_instance=RequestContext(request))
 
+See :ref:`query-string-fields` for more details.
+
 The final approach allows both of the previous approaches to be overridden. The
 instance property ``order_by`` can be
 
@@ -321,6 +323,115 @@ an accessor is used to cross relationships. To get around this be careful to
 define a ``verbose_name`` on such columns.
 
 
+.. _query-string-fields:
+
+Querystring fields
+==================
+
+The table from ``{% render_table %}`` supports sortable columns, and
+pagination. These options are passed via the querystring, and must be passed to
+the table in order for them to have an effect, e.g.
+
+.. code-block:: python
+
+    def people_listing(request):
+        table = PeopleTable(Person.objects.all())
+        table.paginate(page=request.GET.get("page", 1))
+        table.order_by = request.GET.get("sort")
+        return render_to_response("people_listing.html", {"table": table},
+                                  context_instance=RequestContext(request))
+
+This works well unless you have more than one table on a page. In that
+scenarios, all the tables will try to use the same ``sort`` and ``page``
+querystring fields.
+
+In following ``django.forms`` the solution, a prefix can be specified for each
+table:
+
+.. code-block:: python
+
+    def people_listing(request):
+        table1 = PeopleTable(Person.objects.all(), prefix="1-")  # prefix specified
+        table1.paginate(page=request.GET.get("1-page", 1))
+        table1.order_by = request.GET.get("1-sort")
+
+        table2 = PeopleTable(Person.objects.all(), prefix="2-")  # prefix specified
+        table2.paginate(page=request.GET.get("2-page", 1))
+        table2.order_by = request.GET.get("2-sort")
+
+        return render_to_response("people_listing.html",
+                                  {"table1": table1, "table2": table2},
+                                  context_instance=RequestContext(request))
+
+Taking this one step further, rather than just specifying a prefix, it's
+possible to specify the base name for each option. Suppose you don't like the
+name ``sort`` for the ordering option -- you could change it to ``ob``
+(initialism of *order by*). Such options are configured via the ``FOO_field``
+properties:
+
+- ``order_by_field``
+- ``page_field``
+- ``per_page_field`` -- **note:** this field currently isn't used by
+  ``{% render_table %}``
+
+Example:
+
+.. code-block:: python
+
+    def people_listing(request):
+        table = PeopleTable(Person.objects.all(), order_by_field="ob",
+                            page_field="p")
+        table.paginate(page=request.GET.get("p", 1))
+        table.order_by = request.GET.get("ob")
+        return render_to_response("people_listing.html", {"table": table},
+                                  context_instance=RequestContext(request))
+
+In following django-tables2 conventions, these options can be configured in
+different places:
+
+- ``Meta`` class in the table definition.
+- Table constructor.
+- Table instance property.
+
+For convenience there is a set of ``prefixed_FOO_field`` properties that exist
+on each table instance and return the final querystring field names (i.e.
+combines the prefix with the base name). This allows the above view to be
+re-written:
+
+.. code-block:: python
+
+    def people_listing(request):
+        table = PeopleTable(Person.objects.all(), order_by_field="ob",
+                            page_field="p")
+        table.paginate(page=request.GET.get(table.prefixed_page_field, 1))
+        table.order_by = request.GET.get(table.prefixed_order_by_field)
+        return render_to_response("people_listing.html", {"table": table},
+                                  context_instance=RequestContext(request))
+
+
+Config objects
+==============
+
+Config objects make it easier to configure a table. At the moment there's just
+one -- ``RequestConfig``. It takes a ``HttpRequest`` and is able to configure a
+table's sorting and pagination by extracting querystring data.
+
+The view from the previous section can be rewritten without the boilerplate:
+
+.. code-block:: python
+
+    from django_tables2 import RequestConfig
+
+    def people_listing(request):
+        table = PeopleTable(Person.objects.all(), order_by_field="ob",
+                            page_field="p")
+        RequestConfig(request).configure(table)
+        return render_to_response("people_listing.html", {"table": table},
+                                  context_instance=RequestContext(request))
+
+See :class:`.RequestConfig` for details.
+
+
 .. _pagination:
 
 Pagination
@@ -823,7 +934,12 @@ API Reference
 --------------------------
 
 .. autoclass:: django_tables2.utils.Accessor
-    :members:
+
+
+:class:`RequestConfig` Objects:
+-------------------------------
+
+.. autoclass:: django_tables2.config.RequestConfig
 
 
 :class:`Table` Objects:
index e96c9c05f67b710205ff6afe9f148c0f2a9c6b3b..7b7821ed73c9e80ff3aec0c856863e1d46cfd0ed 100644 (file)
@@ -1,15 +1,17 @@
+# -*- coding: utf-8 -*-
 from django.db import models
+from django.utils.translation import ugettext_lazy as _
 
 
 class Country(models.Model):
     """Represents a geographical Country"""
     name = models.CharField(max_length=100)
-    population = models.PositiveIntegerField()
+    population = models.PositiveIntegerField(verbose_name=u"PoblaciĆ³n")
     tz = models.CharField(max_length=50)
     visits = models.PositiveIntegerField()
 
     class Meta:
-        verbose_name_plural = 'Countries'
+        verbose_name_plural = _("Countries")
 
     def __unicode__(self):
         return self.name
index 768550fc4ef2a7072a42a58823aa973ca209eec0..ea70fb3271b0aea8c39f9f154c2ace4f50b9ca87 100644 (file)
@@ -2,21 +2,23 @@ from django.shortcuts import render_to_response
 from django.template import RequestContext
 from .tables import CountryTable, ThemedCountryTable
 from .models import Country
+from django_tables2 import RequestConfig
 
 
 def home(request):
-    order_by = request.GET.get('sort')
-    queryset = Country.objects.all()
-    #
-    example1 = CountryTable(queryset, order_by=order_by)
-    #
-    example2 = CountryTable(queryset, order_by=order_by)
-    example2.paginate(page=request.GET.get('page', 1), per_page=3)
-    #
-    example3 = ThemedCountryTable(queryset, order_by=order_by)
-    #
-    example4 = ThemedCountryTable(queryset, order_by=order_by)
-    example4.paginate(page=request.GET.get('page', 1), per_page=3)
+    qs = Country.objects.all()
+
+    example1 = CountryTable(qs, prefix="1-")
+    RequestConfig(request, paginate=False).configure(example1)
+
+    example2 = CountryTable(qs, prefix="2-")
+    RequestConfig(request, paginate={"per_page": 2}).configure(example2)
+
+    example3 = ThemedCountryTable(qs, prefix="3-")
+    RequestConfig(request, paginate={"per_page": 3}).configure(example3)
+
+    example4 = ThemedCountryTable(qs, prefix="4-")
+    RequestConfig(request, paginate={"per_page": 3}).configure(example4)
 
     return render_to_response('example.html', {
         'example1': example1,
index 47b05b9c6c3e3b21562b0d04155a9c454a2578ce..e77311e32da58a444941bb881eb1a8c5d5def26f 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,7 @@ setup(
     include_package_data=True,  # declarations in MANIFEST.in
 
     install_requires=['Django >=1.1'],
-    tests_require=['Django >=1.1', 'Attest >=0.4', 'django-attest'],
+    tests_require=['Django >=1.1', 'Attest >=0.4', 'django-attest', 'fudge'],
 
     test_loader='attest:FancyReporter.test_loader',
     test_suite='tests.everything',
index 21419be7a7e4a6691110eba1f9daaed80db348cb..701feb0a573adc9ee5e17f5a792830847d2c8c54 100644 (file)
@@ -31,5 +31,6 @@ from .models import models
 from .utils import utils
 from .rows import rows
 from .columns import columns
+from .config import config
 
-everything = Tests([core, templates, models, utils, rows, columns])
+everything = Tests([core, templates, models, utils, rows, columns, config])
diff --git a/tests/config.py b/tests/config.py
new file mode 100644 (file)
index 0000000..0a24b78
--- /dev/null
@@ -0,0 +1,32 @@
+from attest import Tests
+from django_tables2 import RequestConfig
+from django.test.client import RequestFactory
+from fudge import Fake
+
+
+config = Tests()
+
+
+@config.test
+def request_config():
+    factory = RequestFactory()
+    request= factory.get("/?page=1&per_page=5&sort=abc")
+    table = (Fake("Table")
+             .has_attr(prefixed_page_field="page",
+                       prefixed_per_page_field="per_page",
+                       prefixed_order_by_field="sort")
+             .expects("paginate").with_args(page=1, per_page=5)
+             .expects("order_by").with_args("abc"))
+
+    RequestConfig(request).configure(table)
+
+    # Test with some defaults.
+    request= factory.get("/?page=1&sort=abc")
+    table = (Fake("Table")
+             .has_attr(prefixed_page_field="page",
+                       prefixed_per_page_field="per_page",
+                       prefixed_order_by_field="sort")
+             .expects("paginate").with_args(page=1, per_page=5)
+             .expects("order_by").with_args("abc"))
+
+    RequestConfig(request, paginate={"per_page": 5}).configure(table)
index 109d26f0e826b60923d2e53b816c566ca9cb79a3..fa98d60ed599e6b937e8620c2df3a7c1da006162 100644 (file)
@@ -275,3 +275,71 @@ def empty_text():
 
     table = TestTable([], empty_text='still nothing')
     Assert(table.empty_text) == 'still nothing'
+
+
+@core.test
+def prefix():
+    """Test that table prefixes affect the names of querystring parameters"""
+    class TableA(tables.Table):
+        name = tables.Column()
+
+        class Meta:
+            prefix = "x"
+
+    Assert("x") == TableA([]).prefix
+
+    class TableB(tables.Table):
+        name = tables.Column()
+
+    Assert("") == TableB([]).prefix
+    Assert("x") == TableB([], prefix="x").prefix
+
+    table = TableB([])
+    table.prefix = "x"
+    Assert("x") == table.prefix
+
+
+@core.test
+def field_names():
+    class TableA(tables.Table):
+        class Meta:
+            order_by_field = "abc"
+            page_field = "def"
+            per_page_field = "ghi"
+
+    table = TableA([])
+    Assert("abc") == table.order_by_field
+    Assert("def") == table.page_field
+    Assert("ghi") == table.per_page_field
+
+
+@core.test
+def field_names_with_prefix():
+    class TableA(tables.Table):
+        class Meta:
+            order_by_field = "sort"
+            page_field = "page"
+            per_page_field = "per_page"
+            prefix = "1-"
+
+    table = TableA([])
+    Assert("1-sort") == table.prefixed_order_by_field
+    Assert("1-page") == table.prefixed_page_field
+    Assert("1-per_page") == table.prefixed_per_page_field
+
+    class TableB(tables.Table):
+        class Meta:
+            order_by_field = "sort"
+            page_field = "page"
+            per_page_field = "per_page"
+
+    table = TableB([], prefix="1-")
+    Assert("1-sort") == table.prefixed_order_by_field
+    Assert("1-page") == table.prefixed_page_field
+    Assert("1-per_page") == table.prefixed_per_page_field
+
+    table = TableB([])
+    table.prefix = "1-"
+    Assert("1-sort") == table.prefixed_order_by_field
+    Assert("1-page") == table.prefixed_page_field
+    Assert("1-per_page") == table.prefixed_per_page_field
index 54d6b87869f87f75df3eff46241955fc4db1cc83..0f9b5e48d1c42632e1b1fcb2e734f292506c77b1 100644 (file)
@@ -1,6 +1,5 @@
 import itertools
 from django.conf import settings
-from django.test.client import RequestFactory
 from django.template import Template, Context
 import django_tables2 as tables
 from django_attest import TransactionTestContext
index a801c39b328fb203bfa9a7c8ecde6a2a495070e5..a9641873f46f1615c6640a15f65dd105b9392cec 100644 (file)
@@ -1,7 +1,9 @@
 # -*- coding: utf8 -*-
 from django.template import Template, Context, VariableDoesNotExist
+from django.test.client import RequestFactory
 from django.http import HttpRequest
 from django.conf import settings
+from urlparse import parse_qs
 import django_tables2 as tables
 from attest import Tests, Assert
 from xml.etree import ElementTree as ET
@@ -81,7 +83,7 @@ def custom_rendering():
 
 
 @templates.test
-def templatetag():
+def render_table_templatetag():
     # ensure it works with a multi-order-by
     table = CountryTable(MEMORY_DATA, order_by=('name', 'population'))
     t = Template('{% load django_tables2 %}{% render_table table %}')
@@ -116,10 +118,27 @@ def templatetag():
 
     # variable that doesn't exist (issue #8)
     t = Template('{% load django_tables2 %}{% render_table this_doesnt_exist %}')
-    with Assert.raises(VariableDoesNotExist):
+    with Assert.raises(ValueError):
         settings.DEBUG = True
         t.render(Context())
 
     # Should be silent with debug off
     settings.DEBUG = False
     t.render(Context())
+
+
+@templates.test
+def querystring_templatetag():
+    factory = RequestFactory()
+    t = Template('{% load django_tables2 %}{% querystring "name"="Brad" foo.bar=value %}')
+    # Should be something like: ?name=Brad&a=b&c=5&age=21
+    url = t.render(Context({
+        "request": factory.get('/?a=b&name=dog&c=5'),
+        "foo": {"bar": "age"},
+        "value": 21,
+    }))
+    qs = parse_qs(url[1:])  # everything after the ?
+    Assert(qs["name"]) == ["Brad"]
+    Assert(qs["age"]) == ["21"]
+    Assert(qs["a"]) == ["b"]
+    Assert(qs["c"]) == ["5"]