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
from .tables import Table
from .columns import *
+from .config import RequestConfig
--- /dev/null
+# -*- 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)
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`
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)
"""
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``
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.
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
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
@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))
{% 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>
#! -*- coding: utf-8 -*-
+from __future__ import absolute_import
"""
Allows setting/changing/removing of chosen url query string parameters, while
maintaining any existing others.
{% 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
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)
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:]:
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)
@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]))
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
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
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
--------------------------
.. autoclass:: django_tables2.utils.Accessor
- :members:
+
+
+:class:`RequestConfig` Objects:
+-------------------------------
+
+.. autoclass:: django_tables2.config.RequestConfig
:class:`Table` Objects:
+# -*- 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
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,
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',
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])
--- /dev/null
+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)
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
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
# -*- 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
@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 %}')
# 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"]