Fix for #503652 (thanks Zeth). The quasi-internal APIs to retrieve the SQL code for...
[django-tables2.git] / README
diff --git a/README b/README
index 84d9a1b264e402ac549b60ac60d7034ba7cd2258..622785b68f56fc8856d01144fbf202e6023508a7 100644 (file)
--- a/README
+++ b/README
-===============\r
-Ordering Syntax\r
-===============\r
+django-tables\r
+=============\r
 \r
-Works exactly like in the Django database API. Order may be specified as\r
-a list (or tuple) of column names. If prefixed with a hypen, the ordering\r
-for that particular field will be in reverse order.\r
+A Django QuerySet renderer.\r
+\r
+Installation\r
+------------\r
+\r
+Adding django-tables to your INSTALLED_APPS settings is optional, it'll get\r
+you the ability to load some template utilities via {% load tables %}, but\r
+apart from that, ``import django_tables as tables`` should get you going.\r
+\r
+Running the test suite\r
+----------------------\r
+\r
+The test suite uses nose:\r
+    http://somethingaboutorange.com/mrl/projects/nose/\r
+\r
+Working with Tables\r
+-------------------\r
+\r
+A table class looks very much like a form:\r
+\r
+    import django_tables as tables\r
+    class CountryTable(tables.Table):\r
+        name = tables.Column(verbose_name="Country Name")\r
+        population = tables.Column(sortable=False, visible=False)\r
+        time_zone = tables.Column(name="tz", default="UTC+1")\r
+\r
+Instead of fields, you declare a column for every piece of data you want to\r
+expose to the user.\r
+\r
+To use the table, create an instance:\r
+\r
+    countries = CountryTable([{'name': 'Germany', population: 80},\r
+                              {'name': 'France', population: 64}])\r
+\r
+Decide how the table should be sorted:\r
+\r
+    countries.order_by = ('name',)\r
+    assert [row.name for row in countries.row] == ['France', 'Germany']\r
+\r
+    countries.order_by = ('-population',)\r
+    assert [row.name for row in countries.row] == ['Germany', 'France']\r
+\r
+If you pass the table object along into a template, you can do:\r
+\r
+    {% for column in countries.columns %}\r
+        {{column}}\r
+    {% endfor %}\r
+\r
+Which will give you:\r
+\r
+    Country Name\r
+    Timezone\r
+\r
+Note that ``population`` is skipped (as it has ``visible=False``), that the\r
+declared verbose name for the ``name`` column is used, and that ``time_zone``\r
+is converted into a more beautiful string for output automatically.\r
+\r
+There are few requirements for the source data of a table. It should be an\r
+iterable with dict-like objects. Values found in the source data that are\r
+not associated with a column are ignored, missing values are replaced by\r
+the column default or None.\r
+\r
+Common Workflow\r
+~~~~~~~~~~~~~~~\r
+\r
+Usually, you are going to use a table like this. Assuming ``CountryTable``\r
+is defined as above, your view will create an instance and pass it to the\r
+template:\r
+\r
+    def list_countries(request):\r
+        data = ...\r
+        countries = CountryTable(data, order_by=request.GET.get('sort'))\r
+        return render_to_response('list.html', {'table': countries})\r
+\r
+Note that we are giving the incoming "sort" query string value directly to\r
+the table, asking for a sort. All invalid column names will (by default) be\r
+ignored. In this example, only "name" and "tz" are allowed, since:\r
+\r
+    * "population" has sortable=False\r
+    * "time_zone" has it's name overwritten with "tz".\r
+\r
+Then, in the "list.html" template, write:\r
+\r
+    <table>\r
+    <tr>\r
+        {% for column in table.columns %}\r
+        <th><a href="?sort={{ column.name }}">{{ column }}</a></th>\r
+        {% endfor %}\r
+    </tr>\r
+    {% for row in table.rows %}\r
+        <tr>\r
+        {% for value in row %}\r
+            <td>{{ value }}</td>\r
+        {% endfor %}\r
+        </tr>\r
+    {% endfor %}\r
+    </table>\r
+\r
+This will output the data as an HTML table. Note how the table is now fully\r
+sortable, since our link passes along the column name via the querystring,\r
+which in turn will be used by the server for ordering. ``order_by`` accepts\r
+comma-separated strings as input, and "{{ table.order_by }}" will be rendered\r
+as a such a string.\r
+\r
+Instead of the iterator, you can use your knowledge of the table structure to\r
+access columns directly:\r
+\r
+    {% if table.columns.tz.visible %}\r
+        {{ table.columns.tz }}\r
+    {% endfor %}\r
+\r
+\r
+Dynamic Data\r
+~~~~~~~~~~~~\r
+\r
+If any value in the source data is a callable, it will be passed it's own\r
+row instance and is expected to return the actual value for this particular\r
+table cell.\r
+\r
+Similarily, the colunn default value may also be callable that will takes\r
+the row instance as an argument (representing the row that the default is\r
+needed for).\r
+\r
+\r
+Table Options\r
+-------------\r
+\r
+Table-specific options are implemented using the same inner ``Meta`` class\r
+concept as known from forms and models in Django:\r
+\r
+    class MyTable(tables.Table):\r
+        class Meta:\r
+            sortable = True\r
+\r
+Currently, for non-model tables, the only supported option is ``sortable``.\r
+Per default, all columns are sortable, unless a column specifies otherwise.\r
+This meta option allows you to overwrite the global default for the table.\r
+\r
+\r
+ModelTables\r
+-----------\r
+\r
+Like forms, tables can also be used with models:\r
+\r
+    class CountryTable(tables.ModelTable):\r
+        id = tables.Column(sortable=False, visible=False)\r
+        class Meta:\r
+            model = Country\r
+            exclude = ['clicks']\r
+\r
+The resulting table will have one column for each model field, with the\r
+exception of "clicks", which is excluded. The column for "id" is overwritten\r
+to both hide it and deny it sort capability.\r
+\r
+When instantiating a ModelTable, you usually pass it a queryset to provide\r
+the table data:\r
+\r
+    qs = Country.objects.filter(continent="europe")\r
+    countries = CountryTable(qs)\r
+\r
+However, you can also just do:\r
+\r
+    countries = CountryTable()\r
+\r
+and all rows exposed by the default manager of the model the table is based\r
+on will be used.\r
+\r
+If you are using model inheritance, then the following also works:\r
+\r
+    countries = CountryTable(CountrySubclass)\r
+\r
+Note that while you can pass any model, it really only makes sense if the\r
+model also provides fields for the columns you have defined.\r
+\r
+If you just want to use ModelTables, but without auto-generated columns,\r
+you do not have to list all model fields in the ``exclude`` Meta option.\r
+Instead, simply don't specify a model.\r
+\r
+\r
+Custom Columns\r
+~~~~~~~~~~~~~~\r
+\r
+You an add custom columns to your ModelTable that are not based on actual\r
+model fields:\r
+\r
+    class CountryTable(tables.ModelTable):\r
+        custom = tables.Column(default="foo")\r
+        class Meta:\r
+            model = Country\r
+\r
+Just make sure your model objects do provide an attribute with that name.\r
+Functions are also supported, so ``Country.custom`` could be a callable.\r
+\r
+\r
+Spanning relationships\r
+~~~~~~~~~~~~~~~~~~~~~~\r
+\r
+Let's assume you have a ``Country`` model, with a foreignkey ``capital``\r
+pointing to the ``City`` model. While displaying a list of countries,\r
+you might want want to link to the capital's geographic location, which is\r
+stored in ``City.geo`` as a ``(lat, long)`` tuple, on Google Maps.\r
+\r
+ModelTables support relationship spanning syntax of Django's database api:\r
+\r
+    class CountryTable(tables.ModelTable):\r
+        city__geo = tables.Column(name="geo")\r
+\r
+This will add a column named "geo", based on the field by the same name\r
+from the "city" relationship. Note that the name used to define the column\r
+is what will be used to access the data, while the name-overwrite passed to\r
+the column constructor just defines a prettier name for us to work with.\r
+This is to be consistent with auto-generated columns based on model fields,\r
+where the field/column name naturally equals the source name.\r
+\r
+However, to make table defintions more visually appealing and easier to\r
+read, an alternative syntax is supported: setting the column ``data``\r
+property to the appropriate string.\r
+\r
+    class CountryTable(tables.ModelTable):\r
+        geo = tables.Column(data='city__geo')\r
+\r
+Note that you don't need to define a relationship's fields as separate \r
+columns if you already have a column for the relationship itself, i.e.:\r
+\r
+    class CountryTable(tables.ModelTable):\r
+        city = tables.Column()\r
+        \r
+    for country in countries.rows:\r
+        print country.city.id\r
+        print country.city.geo\r
+        print country.city.founder.name\r
+\r
+\r
+ModelTable Specialities\r
+~~~~~~~~~~~~~~~~~~~~~~~\r
+\r
+ModelTables currently have some restrictions with respect to ordering:\r
+\r
+    * Custom columns not based on a model field do not support ordering,\r
+      regardless of the ``sortable`` property (it is ignored).\r
+\r
+    * A ModelTable column's ``default`` or ``data`` value does not affect\r
+      ordering. This differs from the non-model table behaviour.\r
+\r
+If a column is mapped to a method on the model, that method will be called\r
+without arguments. This behavior differs from non-model tables, where a\r
+row object will be passed.\r
+\r
+If you are using callables (e.g. for the ``default`` or ``data`` column\r
+options), they will generally be run when a row is accessed, and\r
+possible repeatetly when accessed more than once. This behavior differs from\r
+non-model tables, where they would be called once, when the table is\r
+generated.\r
+\r
+Columns\r
+-------\r
+\r
+Columns are what defines a table. Therefore, the way you configure your\r
+columns determines to a large extend how your table operates.\r
+\r
+``django_tables.columns`` currently defines three classes, ``Column``,\r
+``TextColumn`` and ``NumberColumn``. However, the two subclasses currently\r
+don't do anything special at all, so you can simply use the base class.\r
+While this will likely change in the future (e.g. when grouping is added),\r
+the base column class will continue to work by itself.\r
+\r
+There are no required arguments. The following is fine:\r
+\r
+    class MyTable(tables.Table):\r
+        c = tables.Column()\r
+\r
+It will result in a column named "c" in the table. You can specify the\r
+``name`` to override this:\r
+\r
+    c = tables.Column(name="count")\r
+\r
+The column is now called and accessed via "count", although the table will\r
+still use "c" to read it's values from the source. You can however modify\r
+that as well, by specifying ``data``:\r
+\r
+    c = tables.Column(name="count", data="count")\r
+\r
+For most practicual purposes, "c" is now meaningless. While in most cases\r
+you will just define your column using the name you want it to have, the\r
+above is useful when working with columns automatically generated from\r
+models:\r
+\r
+    class BookTable(tables.ModelTable):\r
+        book_name = tables.Column(name="name")\r
+        author = tables.Column(data="info__author__name")\r
+        class Meta:\r
+            model = Book\r
+\r
+The overwritten ``book_name`` field/column will now be exposed as the\r
+cleaner "name", and the new "author" column retrieves it's values from\r
+``Book.info.author.name``.\r
+\r
+Note: ``data`` may also be a callable which will be passed a row object.\r
+\r
+Apart from their internal name, you can define a string that will be used\r
+when for display via ``verbose_name``:\r
+\r
+    pubdate = tables.Column(verbose_name="Published")\r
+\r
+The verbose name will be used, for example, if you put in a template:\r
+\r
+    {{ column }}\r
+\r
+If you don't want a column to be sortable by the user:\r
+\r
+    pubdate = tables.Column(sortable=False)\r
+\r
+Sorting is also affected by ``direction``, which can be used to change the\r
+*default* sort direction to descending. Note that this option only indirectly\r
+translates to the actual direction. Normal und reverse order, the terms\r
+django-tables exposes, now simply mean different things.\r
+\r
+    pubdate = tables.Column(direction='desc')\r
+\r
+If you don't want to expose a column (but still require it to exist, for\r
+example because it should be sortable nonetheless):\r
+\r
+    pubdate = tables.Column(visible=False)\r
+\r
+The column and it's values will now be skipped when iterating through the\r
+table, although it can still be accessed manually.\r
+\r
+Finally, you can specify default values for your columns:\r
+\r
+    health_points = tables.Column(default=100)\r
+\r
+Note that how the default is used and when it is applied differs between\r
+static and ModelTables.\r
+\r
+\r
+The table.columns container\r
+---------------------------\r
+\r
+While you can iterate through ``columns`` and get all the currently visible\r
+columns, it further provides features that go beyond a simple iterator.\r
+\r
+You can access all columns, regardless of their visibility, through\r
+``columns.all``.\r
+\r
+``columns.sortable`` is a handy shortcut that exposes all columns which's\r
+``sortable`` attribute is True. This can be very useful in templates, when\r
+doing {% if column.sortable %} can conflict with {{ forloop.last }}.\r
+\r
+\r
+Tables and Pagination\r
+---------------------\r
+\r
+If your table has a large number of rows, you probably want to paginate\r
+the output. There are two distinct approaches.\r
+\r
+First, you can just paginate over ``rows`` as you would do with any other\r
+data:\r
+\r
+    table = MyTable(queryset)\r
+    paginator = Paginator(table.rows, 10)\r
+    page = paginator.page(1)\r
+\r
+You're not necessarily restricted to Django's own paginator (or subclasses) -\r
+any paginator should work with this approach, so long it only requires\r
+``rows`` to implement ``len()``, slicing, and, in the case of ModelTables, a\r
+``count()`` method. The latter means that the ``QuerySetPaginator`` also\r
+works as expected.\r
+\r
+Alternatively, you may use the ``paginate`` feature:\r
+\r
+    table = MyTable(queryset)\r
+    table.paginate(Paginator, 10, page=1, orphans=2)\r
+    for row in table.rows.page():\r
+        pass\r
+    table.paginator                # new attributes\r
+    table.page\r
+\r
+The table will automatically create an instance of ``QuerySetPaginator``,\r
+passing it's own data as the first argument and additionally any arguments\r
+you have specified, except for ``page``. You may use any paginator, as long\r
+as it follows the Django protocol:\r
+\r
+    * Take data as first argument.\r
+    * Support a page() method returning an object with an ``object_list``\r
+      attribute, exposing the paginated data.\r
+\r
+Note that due to the abstraction layer that django-tables represents, it is\r
+not necessary to use Django's QuerySetPaginator with ModelTables. Since the\r
+table knows that it holds a queryset, it will automatically choose to use\r
+count() to determine the data length (which is exactly what\r
+QuerySetPaginator would do).\r
+\r
+Ordering\r
+--------\r
+\r
+The syntax is similar to that of the Django database API. Order may be\r
+specified a list (or tuple) of column names. If prefixed with a hyphen, the\r
+ordering for that particular column will be in reverse order.\r
 \r
 Random ordering is currently not supported.\r
 \r
-====\r
+Interacting with order\r
+~~~~~~~~~~~~~~~~~~~~~~\r
+\r
+Letting the user change the order of a table is a common scenario. With\r
+respect to Django, this means adding links to your table output that will\r
+send off the appropriate arguments to the server. django-tables attempts\r
+to help with you that.\r
+\r
+A bound column, that is a colum accessed through a table instance, provides\r
+the following attributes:\r
+\r
+    - ``name_reversed`` will simply return the column name prefixed with a\r
+      hyphen; this is useful in templates, where string concatenation can\r
+      at times be difficult.\r
+\r
+    - ``name_toggled`` checks the tables current order, and will then\r
+    return the column either prefixed with an hyphen (for reverse ordering)\r
+    or without, giving you the exact opposite order. If the column is\r
+    currently not ordered, it will start off in non-reversed order.\r
+\r
+It is easy to be confused about the difference between the ``reverse`` and\r
+``toggle`` terminology. django-tables tries to put normal/reverse-order\r
+abstraction on top of "ascending/descending", where as normal order could\r
+potentially mean either ascending or descending, depending on the column.\r
+\r
+Commonly, you see tables that indicate what columns they are currently\r
+ordered by using little arrows. To implement this:\r
+\r
+    - ``is_ordered``: Returns True if the column is in the current\r
+    ``order_by``, regardless of the polarity.\r
+\r
+    - ``is_ordered_reverse``, ``is_ordered_straight``: Returns True if the\r
+    column is ordered in reverse or non-reverse, respectively, otherwise\r
+    False.\r
+\r
+The above is usually enough for most simple cases, where tables are only\r
+ordered by a single column. For scenarios in which multi-column order is\r
+used, additional attributes are available:\r
+\r
+    - ``order_by``: Return the current order, but with the current column\r
+    set to normal ordering. If the current column is not already part of\r
+    the order, it is appended. Any existing columns in the order are\r
+    maintained as-is.\r
+\r
+    - ``order_by_reversed``, ``order_by_toggled``: Similarly, return the\r
+    table's current ``order_by`` with the column set to reversed or toggled,\r
+    respectively. Again, it is appended if not already ordered.\r
+\r
+Additionally, ``table.order_by.toggle()`` may also be useful in some cases:\r
+It will toggle all order columns and should thus give you the exact\r
+opposite order.\r
+\r
+The following is a simple example of single-column ordering. It shows a list\r
+of sortable columns, each clickable, and an up/down arrow next to the one\r
+that is currently used to sort the table.\r
+\r
+    Sort by:\r
+    {% for column in table.columns %}\r
+        {% if column.sortable %}\r
+            <a href="?sort={{ column.name_toggled }}">{{ column }}</a>\r
+            {% if column.is_ordered_straight %}<img src="down.png" />{% endif %}\r
+            {% if column.is_ordered_reverse %}<img src="up.png" />{% endif %}\r
+        {% endif %}\r
+    {% endfor %}\r
+\r
+\r
+Error handling\r
+--------------\r
+\r
+Passing incoming query string values from the request directly to the\r
+table constructor is a common thing to do. However, such data can easily\r
+be invalid, be it that a user manually modified it, or someone put up a\r
+broken link. In those cases, you usually would not want to raise an\r
+exception (nor be notified by Django's error notification mechanism) -\r
+there is nothing you could do anyway.\r
+\r
+Because of this, such errors will by default be silently ignored. For\r
+example, if one out of three columns in an "order_by" is invalid, the other\r
+two will still be applied:\r
+\r
+    table.order_by = ('name', 'totallynotacolumn', '-date)\r
+    assert table.order_by = ('name', '-date)\r
+\r
+This ensures that the following table will be created regardless of the\r
+string in "sort.\r
+\r
+    table = MyTable(data, order_by=request.GET.get('sort'))\r
+\r
+However, if you want, you can disable this behaviour and have an exception\r
+raised instead, using:\r
+\r
+    import django_tables\r
+    django_tables.options.IGNORE_INVALID_OPTIONS = False\r
+\r
+\r
+Template Utilities\r
+------------------\r
+\r
+If you want the give your users the ability to interact with your table (e.g.\r
+change the ordering), you will need to create urls with the appropriate\r
+queries. To simplify that process, django-tables comes with a helpful\r
+templatetag:\r
+\r
+    {% set_url_param sort="name" %}       # ?sort=name\r
+    {% set_url_param sort="" %}           # delete "sort" param\r
+\r
+The template library can be found in 'django_modules.app.templates.tables'.\r
+If you add ''django_modules.app' to your INSTALLED_APPS setting, you will\r
+be able to do:\r
+\r
+    {% load tables %}\r
+    \r
+Note: The tag requires the current request to be available as ``request`` \r
+in the context (usually, this means activating the Django request context\r
+processor).\r
+\r
+\r
 TODO\r
-====\r
+----\r
+    - "data", if used to format for display, affect sorting; this stuff needs\r
+      some serious redesign.\r
+    - as_html methods are all empty right now\r
+    - table.column[].values is a stub\r
+    - filters\r
+    - grouping\r
+    - choices support for columns (internal column value will be looked up\r
+      for output)\r
+    - for columns that span model relationships, automatically generate\r
+      select_related(); this is important, since right now such an e.g.\r
+      order_by would cause rows to be dropped (inner join).\r
+    - initialize auto-generated columns with the relevant properties of the\r
+      model fields (verbose_name, editable=visible?, ...)\r
+    - remove support for callable fields? this has become obsolete since we\r
+      Column.data property; also, it's easy to make the call manually, or let\r
+      the template engine handle it\r
+    - tests could use some refactoring, they are currently all over the place\r
+    - what happens if duplicate column names are used? we currently don't\r
+      check for that at all\r
+\r
+Filters\r
+~~~~~~~\r
+\r
+Filtering is already easy (just use the normal queryset methods), but\r
+filter support in django-tables would want to hide the Django ORM syntax\r
+from the user.\r
+\r
+    * For example, say a ``models.DateTimeField`` should be filtered\r
+      by year: the user would only see ``date=2008`` rather than maybe\r
+      ``published_at__year=2008``.\r
+\r
+    * Say you want to filter out ``UserProfile`` rows that do not have\r
+      an avatar image set. The user would only see ```no_avatar``, which\r
+      in Django ORM syntax might map to\r
+      ``Q(avatar__isnull=True) | Q(avatar='')``.\r
+\r
+Filters would probably always belong to a column, and be defined along\r
+side one.\r
+\r
+    class BookTable(tables.ModelTable):\r
+        date = tables.Column(filter='published_at__year')\r
+\r
+If a filter is needed that does not belong to a single colunn, a column\r
+would have to be defined for just that filter. A ``tables.Filter`` object\r
+could be provided that would basically be a column, but with default\r
+properties set so that the column functionality is disabled as far as\r
+possible (i.e. ``visible=False`` etc):\r
+\r
+    class BookTable(tables.ModelTable):\r
+        date = tables.Column(filter='published_at__year')\r
+        has_cover = tables.Filter('cover__isnull', value=True)\r
+\r
+Or, if Filter() gets a lot of additional functionality like ``value``,\r
+we could generally make it available to all filters like so:\r
+\r
+    class BookTable(tables.ModelTable):\r
+        date = tables.Column(filter=tables.Filter('published_at__year', default=2008))\r
+        has_cover = tables.Filter('cover__isnull', value=True)\r
+\r
+More complex filters should be supported to (e.g. combine multiple Q\r
+objects, support ``exclude`` as well as ``filter``). Allowing the user\r
+to somehow specify a callable probably is the easiest way to enable this.\r
+\r
+The filter querystring syntax, as exposed to the user, could look like this:\r
+\r
+    /view/?filter=name:value\r
+    /view/?filter=name\r
+\r
+It would also be cool if filters could be combined. However, in that case\r
+it would also make sense to make it possible to choose individual filters\r
+which cannot be combined with any others, or maybe even allow the user\r
+to specify complex dependencies. That may be pushing it though, and anyway\r
+won't make it into the first version.\r
+\r
+    /view/?filter=name:value,foo:bar\r
+\r
+We need to think about how we make the functionality available to\r
+templates.\r
+\r
+Another feature that would be important is the ability to limit the valid\r
+values for a filter, e.g. in the date example only years from 2000 to 2008.\r
+\r
+Use django-filters:\r
+    - would support html output\r
+    - would not work at all with our planned QueryTable\r
+    - conflicts somewhat in that it also allows ordering\r
+    \r
+To autoamtically allow filtering a column with filter=True, we would need to\r
+have subclasses for each model class, even if it just redirects to use the \r
+correct filter class; \r
 \r
- - Support table filters\r
- - Support grouping\r
- - Support choices-like data
\ No newline at end of file
+If not using django-filter, we wouldn't have different filter types; filters \r
+would just hold the data, and each column would know how to apply it.
\ No newline at end of file