From 7c82e63873fb122c9a3b2c879d3151a302f93a86 Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Thu, 12 May 2011 07:02:26 +1000 Subject: [PATCH] * Added example project to demonstrate usage. * {% render_table %} now raises an exception if a RequestContext 'request' isn't in the template context. * Added better documentation in the "Slow Start Guide" about pagination and ordering * Fixed some styling issues with the paleblue theme * Added instructions on how to build the docs when using a virtualenv * bumped version to v0.4.1 --- README.rst | 8 + django_tables/columns.py | 4 +- .../themes/paleblue/css/screen.css | 9 +- django_tables/templatetags/django_tables.py | 17 +- docs/conf.py | 4 +- docs/index.rst | 95 ++++++++-- example/__init__.py | 0 example/app/__init__.py | 0 example/app/admin.py | 5 + example/app/fixtures/initial_data.json | 42 +++++ example/app/models.py | 15 ++ example/app/tables.py | 13 ++ example/app/tests.py | 16 ++ example/app/views.py | 26 +++ example/manage.py | 14 ++ example/settings.py | 163 ++++++++++++++++++ example/templates/example.html | 65 +++++++ example/urls.py | 16 ++ setup.py | 2 +- 19 files changed, 491 insertions(+), 23 deletions(-) create mode 100644 example/__init__.py create mode 100644 example/app/__init__.py create mode 100644 example/app/admin.py create mode 100644 example/app/fixtures/initial_data.json create mode 100644 example/app/models.py create mode 100644 example/app/tables.py create mode 100644 example/app/tests.py create mode 100644 example/app/views.py create mode 100644 example/manage.py create mode 100644 example/settings.py create mode 100644 example/templates/example.html create mode 100644 example/urls.py diff --git a/README.rst b/README.rst index 248107e..171ecfd 100644 --- a/README.rst +++ b/README.rst @@ -9,3 +9,11 @@ has native support for pagination and sorting. It does for HTML tables what Documentation_ is available on http://readthedocs.org .. _Documentation: http://readthedocs.org/docs/django-tables/en/latest/ + + +Building the documentation +========================== + +If you want to build the docs from within a virtualenv, use:: + + make html SPHINXBUILD="python $(which sphinx-build)" diff --git a/django_tables/columns.py b/django_tables/columns.py index 1af2a81..bd7ce92 100644 --- a/django_tables/columns.py +++ b/django_tables/columns.py @@ -274,13 +274,13 @@ class TemplateColumn(Column): Both columns will have the same output. - .. important:: + In order to use template tags or filters that require a ``RequestContext``, the table **must** be rendered via :ref:`{% render_table %} `. - """ + """ def __init__(self, template_code=None, **extra): super(TemplateColumn, self).__init__(**extra) self.template_code = template_code diff --git a/django_tables/static/django_tables/themes/paleblue/css/screen.css b/django_tables/static/django_tables/themes/paleblue/css/screen.css index 767e041..8c26adc 100644 --- a/django_tables/static/django_tables/themes/paleblue/css/screen.css +++ b/django_tables/static/django_tables/themes/paleblue/css/screen.css @@ -2,7 +2,11 @@ table.paleblue { border-collapse: collapse; border-color: #CCC; border: 1px solid #DDD; - font-family: 'Lucida Grande', Verdana, Arial, sans-serif; +} + +table.paleblue, +table.paleblue + ul.pagination { + font: normal 11px/14px 'Lucida Grande', Verdana, Arial, sans-serif; } table.paleblue a:link, @@ -23,6 +27,7 @@ table.paleblue th { line-height: 13px; border-bottom: 1px solid #EEE; border-left: 1px solid #DDD; + text-align: left; } table.paleblue thead th:first-child, @@ -69,8 +74,10 @@ table.paleblue tr.even { table.paleblue + ul.pagination { background: white url(../img/pagination-bg.gif) left 180% repeat-x; overflow: auto; + margin: 0; padding: 10px; border: 1px solid #DDD; + list-style: none; } table.paleblue + ul.pagination > li { diff --git a/django_tables/templatetags/django_tables.py b/django_tables/templatetags/django_tables.py index 3bb1f09..533cc26 100644 --- a/django_tables/templatetags/django_tables.py +++ b/django_tables/templatetags/django_tables.py @@ -90,14 +90,17 @@ class RenderTableNode(template.Node): def render(self, context): table = self.table_var.resolve(context) - request = context.get('request', None) - context = template.Context({'request': request, 'table': table}) + 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}) try: - table.request = request + table.request = context['request'] return get_template('django_tables/table.html').render(context) finally: - pass - #del table.request + del table.request @register.tag @@ -105,6 +108,6 @@ def render_table(parser, token): try: _, table_var_name = token.contents.split() except ValueError: - raise template.TemplateSyntaxError,\ - "%r tag requires a single argument" % token.contents.split()[0] + raise (template.TemplateSyntaxError, + '%r tag requires a single argument' % token.contents.split()[0]) return RenderTableNode(table_var_name) diff --git a/docs/conf.py b/docs/conf.py index 5f5051a..e8e8675 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ project = u'django-tables' # built documents. # # The short X.Y version. -version = '0.4.0' +version = '0.4.1' # The full version, including alpha/beta/rc tags. -release = '0.4.0' +release = '0.4.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index 53c1a40..7a471a6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -90,16 +90,36 @@ In your template, the easiest way to :term:`render` the table is via the | Mexico | 107 | UTC -6 | 0 | +--------------+------------+---------+--------+ -This approach is easy, but it's not fully featured. For slightly more effort, -you can render a table with sortable columns. For this, you must use the -template tag. +This approach is easy, but it's not fully featured (e.g. no pagination, no +sorting). Don't worry it's very easy to add these. First, you must render the +table via the :ref:`template tag ` rather than +``as_html()``: .. code-block:: django {% load django_tables %} {% render_table table %} -See :ref:`template-tags.render_table` for more information. +.. note:: + + ``{% render_table %}`` requires that the ``TEMPLATE_CONTEXT_PROCESSORS`` + setting contains ``"django.core.context_processors.request"``. See + :ref:`template-tags.render_table` for details. + +This is all that's required for the template, but in the view you'll need to +tell the table to which column to order by, and which page of data to display +(pagination). This is achieved as follows: + +.. code-block:: python + + def home(request): + countries = Country.objects.all() + table = CountryTable(countries, order_by=request.GET.get('sort')) + table.paginate(page=request.GET.get('page', 1)) + return render_to_response('home.html', {'table': table}, + context_instance=RequestContext(request)) + +See :ref:`ordering`, and :ref:`pagination` for more information. The table will be rendered, but chances are it will still look quite ugly. An easy way to make it pretty is to use the built-in *paleblue* theme. For this to @@ -167,8 +187,8 @@ same thing: class Meta: order_by = 'name' -The following allows the ``Meta.order_by`` option to be overridden on a -per-instance basis. +By passing in a value for ``order_by`` into the ``Table`` constructor, the +``Meta.order_by`` option can be overridden on a per-instance basis. .. code-block:: python @@ -177,7 +197,21 @@ per-instance basis. table = SimpleTable(..., order_by='name') -Finally the attribute method overrides both of the previous approaches. +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 +means your view will need to look something like: + +.. code-block:: python + + def home(request): + countries = Country.objects.all() + table = CountryTable(countries, order_by=request.GET.get('sort')) + return render_to_response('home.html', {'table': table}, + context_instance=RequestContext(request)) + +The final approach allows both of the previous approaches to be overridden. The +instance property ``order_by`` can be .. code-block:: python @@ -189,9 +223,30 @@ Finally the attribute method overrides both of the previous approaches. ---- -By default all table columns support sorting. This means that the headers for -columns are rendered as links which allow that column to be toggled as the -between ascending and descending ordering preference. +By default all table columns support sorting. This means that if you're using +the :ref:`template tag ` to render the table, +users can sort the table based on any column by clicking the corresponding +header link. + +In some cases this may not be appropriate. For example you may have a column +which displays data that isn't in the dataset: + +.. code-block:: python + + class SimpleTable(tables.Table): + name = tables.Column() + lucky_rating = tables.Column() + + class Meta: + sortable = False + + def render_lucky_rating(self): + import random + return random.randint(1, 10) + +In these types of scenarios, it's simply not possible to sort the table based +on column data that isn't in the dataset. The solution is to disable sorting +for these columns. Sorting can be disabled on a column, table, or table instance basis via the :attr:`.Table.Meta.sortable` option. @@ -448,6 +503,26 @@ rendered. It adds a ``request`` attribute to the table, which allows :class:`Column` objects to have access to a ``RequestContext``. See :class:`.TemplateColumn` for an example. +This tag requires that the template in which it's rendered contains the +``HttpRequest`` inside a ``request`` variable. This can be achieved by ensuring +the ``TEMPLATE_CONTEXT_PROCESSORS`` setting contains +``"django.core.context_processors.request"``. By default it is not included, +and the setting itself is not even defined within your project's +``settings.py``. To resolve this simply add the following to your +``settings.py``: + +.. code-block:: python + + TEMPLATE_CONTEXT_PROCESSORS = ( + "django.contrib.auth.context_processors.auth", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.media", + "django.core.context_processors.static", + "django.contrib.messages.context_processors.messages", + "django.core.context_processors.request", + ) + .. _template-tags.set_url_param: diff --git a/example/__init__.py b/example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/app/__init__.py b/example/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/app/admin.py b/example/app/admin.py new file mode 100644 index 0000000..ec69c11 --- /dev/null +++ b/example/app/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import Country + + +admin.site.register(Country) diff --git a/example/app/fixtures/initial_data.json b/example/app/fixtures/initial_data.json new file mode 100644 index 0000000..93f8531 --- /dev/null +++ b/example/app/fixtures/initial_data.json @@ -0,0 +1,42 @@ +[ + { + "pk": 1, + "model": "app.country", + "fields": { + "tz": "Australia/Brisbane", + "name": "Australia", + "visits": 2, + "population": 20000000 + } + }, + { + "pk": 2, + "model": "app.country", + "fields": { + "tz": "NZST", + "name": "New Zealand", + "visits": 1, + "population": 12000000 + } + }, + { + "pk": 3, + "model": "app.country", + "fields": { + "tz": "CAT", + "name": "Africa", + "visits": 0, + "population": 1000010000 + } + }, + { + "pk": 4, + "model": "app.country", + "fields": { + "tz": "UTC\u22123.5", + "name": "Canada", + "visits": 1, + "population": 34447000 + } + } +] \ No newline at end of file diff --git a/example/app/models.py b/example/app/models.py new file mode 100644 index 0000000..e96c9c0 --- /dev/null +++ b/example/app/models.py @@ -0,0 +1,15 @@ +from django.db import models + + +class Country(models.Model): + """Represents a geographical Country""" + name = models.CharField(max_length=100) + population = models.PositiveIntegerField() + tz = models.CharField(max_length=50) + visits = models.PositiveIntegerField() + + class Meta: + verbose_name_plural = 'Countries' + + def __unicode__(self): + return self.name diff --git a/example/app/tables.py b/example/app/tables.py new file mode 100644 index 0000000..522e666 --- /dev/null +++ b/example/app/tables.py @@ -0,0 +1,13 @@ +import django_tables as tables + + +class CountryTable(tables.Table): + name = tables.Column() + population = tables.Column() + tz = tables.Column(verbose_name='Time Zone') + visits = tables.Column() + + +class ThemedCountryTable(CountryTable): + class Meta: + attrs = {'class': 'paleblue'} diff --git a/example/app/tests.py b/example/app/tests.py new file mode 100644 index 0000000..501deb7 --- /dev/null +++ b/example/app/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/example/app/views.py b/example/app/views.py new file mode 100644 index 0000000..768550f --- /dev/null +++ b/example/app/views.py @@ -0,0 +1,26 @@ +from django.shortcuts import render_to_response +from django.template import RequestContext +from .tables import CountryTable, ThemedCountryTable +from .models import Country + + +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) + + return render_to_response('example.html', { + 'example1': example1, + 'example2': example2, + 'example3': example3, + 'example4': example4, + }, context_instance=RequestContext(request)) diff --git a/example/manage.py b/example/manage.py new file mode 100644 index 0000000..3e4eedc --- /dev/null +++ b/example/manage.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +import imp +try: + imp.find_module('settings') # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) + sys.exit(1) + +import settings + +if __name__ == "__main__": + execute_manager(settings) diff --git a/example/settings.py b/example/settings.py new file mode 100644 index 0000000..3a2a28a --- /dev/null +++ b/example/settings.py @@ -0,0 +1,163 @@ +# import django_tables +from os.path import dirname, join, abspath +import sys + + +ROOT = dirname(abspath(__file__)) + + +sys.path.insert(0, join(ROOT, '..')) +import django_tables +sys.path.pop(0) + + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': join(ROOT, 'database.sqlite'), # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = '' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# URL prefix for admin static files -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '=nzw@mkqk)tz+_#vf%li&8sn7yn8z7!2-4njuyf1rxs*^muhvh' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +TEMPLATE_CONTEXT_PROCESSORS = ( + "django.contrib.auth.context_processors.auth", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.media", + "django.core.context_processors.static", + "django.contrib.messages.context_processors.messages", + "django.core.context_processors.request", +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'example.urls' + +TEMPLATE_DIRS = ( + join(ROOT, 'templates'), +) + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'example.app', + 'django_tables', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} diff --git a/example/templates/example.html b/example/templates/example.html new file mode 100644 index 0000000..d444881 --- /dev/null +++ b/example/templates/example.html @@ -0,0 +1,65 @@ + + + + django-tables examples + + + + + +

django-tables examples

+

This page demonstrates various types of tables being rendered via + django-tables.

+ +

Example 1 — QuerySet

+

via as_html()

+
{% templatetag openvariable %} example1.as_html {% templatetag closevariable %}
+ {{ example1.as_html }} + +

via template tag

+
{% templatetag openblock %} load django_tables {% templatetag closeblock %}
+{% templatetag openblock %} render_table example1 {% templatetag closeblock %}
+ {% load django_tables %} + {% render_table example1 %} + +

Example 2 — QuerySet + pagination

+

via as_html()

+
{% templatetag openvariable %} example2.as_html {% templatetag closevariable %}
+ {{ example2.as_html }} + +

via template tag

+
{% templatetag openblock %} load django_tables {% templatetag closeblock %}
+{% templatetag openblock %} render_table example2 {% templatetag closeblock %}
+ {% load django_tables %} + {% render_table example2 %} + +

Example 3 — QuerySet + paleblue theme

+

via as_html()

+
{% templatetag openvariable %} example3.as_html {% templatetag closevariable %}
+ {{ example3.as_html }} + +

via template tag

+
{% templatetag openblock %} load django_tables {% templatetag closeblock %}
+{% templatetag openblock %} render_table example3 {% templatetag closeblock %}
+ {% load django_tables %} + {% render_table example3 %} + +

Example 4 — QuerySet + pagination + paleblue theme

+

via as_html()

+
{% templatetag openvariable %} example4.as_html {% templatetag closevariable %}
+ {{ example4.as_html }} + +

via template tag

+
{% templatetag openblock %} load django_tables {% templatetag closeblock %}
+{% templatetag openblock %} render_table example4 {% templatetag closeblock %}
+ {% load django_tables %} + {% render_table example4 %} + + + diff --git a/example/urls.py b/example/urls.py new file mode 100644 index 0000000..2c3fefb --- /dev/null +++ b/example/urls.py @@ -0,0 +1,16 @@ +from django.conf.urls.defaults import patterns, include, url + + +from django.contrib import admin +admin.autodiscover() + + +urlpatterns = patterns('', + url(r'^$', 'example.app.views.home', name='home'), + + # Uncomment the admin/doc line below to enable admin documentation: + url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Uncomment the next line to enable the admin: + url(r'^admin/', include(admin.site.urls)), +) diff --git a/setup.py b/setup.py index c2d88bf..5842fce 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup, find_packages setup( name='django-tables', - version='0.4.0', + version='0.4.1', description='Table framework for Django', author='Bradley Ayers', -- 2.26.2