From 86405697cd5f669753a41c60bbc55c97088c0767 Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Sun, 17 Jul 2011 21:10:28 +1000 Subject: [PATCH] README more appealing (has feature list), added RequestConfig, added ability to specify querystring field names and table prefixes (issue #5) --- README.rst | 12 + django_tables2/__init__.py | 1 + django_tables2/config.py | 38 ++++ django_tables2/tables.py | 215 +++++++++++++----- .../templates/django_tables2/table.html | 6 +- django_tables2/templatetags/django_tables2.py | 147 ++++++++---- docs/index.rst | 120 +++++++++- example/app/models.py | 6 +- example/app/views.py | 26 ++- setup.py | 2 +- tests/__init__.py | 3 +- tests/config.py | 32 +++ tests/core.py | 68 ++++++ tests/models.py | 1 - tests/templates.py | 23 +- 15 files changed, 577 insertions(+), 123 deletions(-) create mode 100644 django_tables2/config.py create mode 100644 tests/config.py diff --git a/README.rst b/README.rst index d757787..65244bf 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/django_tables2/__init__.py b/django_tables2/__init__.py index 3437774..5733c1d 100644 --- a/django_tables2/__init__.py +++ b/django_tables2/__init__.py @@ -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 index 0000000..3ec178e --- /dev/null +++ b/django_tables2/config.py @@ -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) diff --git a/django_tables2/tables.py b/django_tables2/tables.py index abe49de..d5d8720 100644 --- a/django_tables2/tables.py +++ b/django_tables2/tables.py @@ -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 ```` 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 ``-``. + + :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 ``
`` 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 ``
`` 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)) diff --git a/django_tables2/templates/django_tables2/table.html b/django_tables2/templates/django_tables2/table.html index d580a1a..3a9bac4 100644 --- a/django_tables2/templates/django_tables2/table.html +++ b/django_tables2/templates/django_tables2/table.html @@ -9,7 +9,7 @@ {% for column in table.columns %} {% if column.sortable %} {% with column.order_by as ob %} - + {% endwith %} {% else %} @@ -34,11 +34,11 @@ {% if table.page %} diff --git a/django_tables2/templatetags/django_tables2.py b/django_tables2/templatetags/django_tables2.py index 3b1a6a5..6107072 100644 --- a/django_tables2/templatetags/django_tables2.py +++ b/django_tables2/templatetags/django_tables2.py @@ -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])) diff --git a/docs/index.rst b/docs/index.rst index 865ca60..86c3bc7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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: diff --git a/example/app/models.py b/example/app/models.py index e96c9c0..7b7821e 100644 --- a/example/app/models.py +++ b/example/app/models.py @@ -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 diff --git a/example/app/views.py b/example/app/views.py index 768550f..ea70fb3 100644 --- a/example/app/views.py +++ b/example/app/views.py @@ -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, diff --git a/setup.py b/setup.py index 47b05b9..e77311e 100755 --- 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', diff --git a/tests/__init__.py b/tests/__init__.py index 21419be..701feb0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -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 index 0000000..0a24b78 --- /dev/null +++ b/tests/config.py @@ -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) diff --git a/tests/core.py b/tests/core.py index 109d26f..fa98d60 100644 --- a/tests/core.py +++ b/tests/core.py @@ -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 diff --git a/tests/models.py b/tests/models.py index 54d6b87..0f9b5e4 100644 --- a/tests/models.py +++ b/tests/models.py @@ -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 diff --git a/tests/templates.py b/tests/templates.py index a801c39..a964187 100644 --- a/tests/templates.py +++ b/tests/templates.py @@ -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"] -- 2.26.2
{{ column.header }}{{ column.header }}{{ column.header }}