For a ModelTable you can use the columns meta option to indicate the column order.
authorHarro van der Klauw <hvdklauw@gmail.com>
Wed, 21 Jul 2010 07:01:12 +0000 (09:01 +0200)
committerHarro van der Klauw <hvdklauw@MacKlauw.local>
Wed, 21 Jul 2010 07:02:45 +0000 (09:02 +0200)
django_tables/models.py

index 7c7e87f09d37d612e56178f6d9f826415ba9a817..882d42c304d3ce3bafe28acb4aa609a9dfd932ca 100644 (file)
-from django.core.exceptions import FieldError\r
-from django.utils.datastructures import SortedDict\r
-from base import BaseTable, DeclarativeColumnsMetaclass, \\r
-    Column, BoundRow, Rows, TableOptions, rmprefix, toggleprefix\r
-\r
-\r
-__all__ = ('ModelTable',)\r
-\r
-\r
-class ModelTableOptions(TableOptions):\r
-    def __init__(self, options=None):\r
-        super(ModelTableOptions, self).__init__(options)\r
-        self.model = getattr(options, 'model', None)\r
-        self.columns = getattr(options, 'columns', None)\r
-        self.exclude = getattr(options, 'exclude', None)\r
-\r
-\r
-def columns_for_model(model, columns=None, exclude=None):\r
-    """\r
-    Returns a ``SortedDict`` containing form columns for the given model.\r
-\r
-    ``columns`` is an optional list of field names. If provided, only the\r
-    named model fields will be included in the returned column list.\r
-\r
-    ``exclude`` is an optional list of field names. If provided, the named\r
-    model fields will be excluded from the returned list of columns, even\r
-    if they are listed in the ``fields`` argument.\r
-    """\r
-\r
-    field_list = []\r
-    opts = model._meta\r
-    for f in opts.fields + opts.many_to_many:\r
-        if (columns and not f.name in columns) or \\r
-           (exclude and f.name in exclude):\r
-            continue\r
-        column = Column() # TODO: chose correct column type, with right options\r
-        if column:\r
-            field_list.append((f.name, column))\r
-    return SortedDict(field_list)\r
-\r
-\r
-class BoundModelRow(BoundRow):\r
-    """Special version of the BoundRow class that can handle model instances\r
-    as data.\r
-\r
-    We could simply have ModelTable spawn the normal BoundRow objects\r
-    with the instance converted to a dict instead. However, this way allows\r
-    us to support non-field attributes and methods on the model as well.\r
-    """\r
-\r
-    def _default_render(self, boundcol):\r
-        """In the case of a model table, the accessor may use ``__`` to\r
-        span instances. We need to resolve this.\r
-        """\r
-        # try to resolve relationships spanning attributes\r
-        bits = boundcol.accessor.split('__')\r
-        current = self.data\r
-        for bit in bits:\r
-            # note the difference between the attribute being None and not\r
-            # existing at all; assume "value doesn't exist" in the former\r
-            # (e.g. a relationship has no value), raise error in the latter.\r
-            # a more proper solution perhaps would look at the model meta\r
-            # data instead to find out whether a relationship is valid; see\r
-            # also ``_validate_column_name``, where such a mechanism is\r
-            # already implemented).\r
-            if not hasattr(current, bit):\r
-                raise ValueError("Could not resolve %s from %s" % (bit, name))\r
-\r
-            current = getattr(current, bit)\r
-            if callable(current):\r
-                current = current()\r
-            # important that we break in None case, or a relationship\r
-            # spanning across a null-key will raise an exception in the\r
-            # next iteration, instead of defaulting.\r
-            if current is None:\r
-                break\r
-\r
-        if current is None:\r
-            # ...the whole name (i.e. the last bit) resulted in None\r
-            if boundcol.column.default is not None:\r
-                return boundcol.get_default(self)\r
-        return current\r
-\r
-\r
-class ModelRows(Rows):\r
-    row_class = BoundModelRow\r
-\r
-    def __init__(self, *args, **kwargs):\r
-        super(ModelRows, self).__init__(*args, **kwargs)\r
-\r
-    def _reset(self):\r
-        self._length = None\r
-\r
-    def __len__(self):\r
-        """Use the queryset count() method to get the length, instead of\r
-        loading all results into memory. This allows, for example,\r
-        smart paginators that use len() to perform better.\r
-        """\r
-        if getattr(self, '_length', None) is None:\r
-            self._length = self.table.data.count()\r
-        return self._length\r
-\r
-    # for compatibility with QuerySetPaginator\r
-    count = __len__\r
-\r
-\r
-class ModelTableMetaclass(DeclarativeColumnsMetaclass):\r
-    def __new__(cls, name, bases, attrs):\r
-        # Let the default form meta class get the declared columns; store\r
-        # those in a separate attribute so that ModelTable inheritance with\r
-        # differing models works as expected (the behaviour known from\r
-        # ModelForms).\r
-        self = super(ModelTableMetaclass, cls).__new__(\r
-            cls, name, bases, attrs, parent_cols_from='declared_columns')\r
-        self.declared_columns = self.base_columns\r
-\r
-        opts = self._meta = ModelTableOptions(getattr(self, 'Meta', None))\r
-        # if a model is defined, then build a list of default columns and\r
-        # let the declared columns override them.\r
-        if opts.model:\r
-            columns = columns_for_model(opts.model, opts.columns, opts.exclude)\r
-            columns.update(self.declared_columns)\r
-            self.base_columns = columns\r
-        return self\r
-\r
-\r
-class ModelTable(BaseTable):\r
-    """Table that is based on a model.\r
-\r
-    Similar to ModelForm, a column will automatically be created for all\r
-    the model's fields. You can modify this behaviour with a inner Meta\r
-    class:\r
-\r
-        class MyTable(ModelTable):\r
-            class Meta:\r
-                model = MyModel\r
-                exclude = ['fields', 'to', 'exclude']\r
-                columns = ['fields', 'to', 'include']\r
-\r
-    One difference to a normal table is the initial data argument. It can\r
-    be a queryset or a model (it's default manager will be used). If you\r
-    just don't any data at all, the model the table is based on will\r
-    provide it.\r
-    """\r
-\r
-    __metaclass__ = ModelTableMetaclass\r
-\r
-    rows_class = ModelRows\r
-\r
-    def __init__(self, data=None, *args, **kwargs):\r
-        if data == None:\r
-            if self._meta.model is None:\r
-                raise ValueError('Table without a model association needs '\r
-                    'to be initialized with data')\r
-            self.queryset = self._meta.model._default_manager.all()\r
-        elif hasattr(data, '_default_manager'): # saves us db.models import\r
-            self.queryset = data._default_manager.all()\r
-        else:\r
-            self.queryset = data\r
-\r
-        super(ModelTable, self).__init__(self.queryset, *args, **kwargs)\r
-\r
-    def _validate_column_name(self, name, purpose):\r
-        """Overridden. Only allow model-based fields and valid model\r
-        spanning relationships to be sorted."""\r
-\r
-        # let the base class sort out the easy ones\r
-        result = super(ModelTable, self)._validate_column_name(name, purpose)\r
-        if not result:\r
-            return False\r
-\r
-        if purpose == 'order_by':\r
-            column = self.columns[name]\r
-\r
-            # "data" can really be used in two different ways. It is\r
-            # slightly confusing and potentially should be changed.\r
-            # It can either refer to an attribute/field which the table\r
-            # column should represent, or can be a callable (or a string\r
-            # pointing to a callable attribute) that is used to render to\r
-            # cell. The difference is that in the latter case, there may\r
-            # still be an actual source model field behind the column,\r
-            # stored in "declared_name". In other words, we want to filter\r
-            # out column names that are not oderable, and the column name\r
-            # we need to check may either be stored in "data" or in\r
-            # "declared_name", depending on if and what kind of value is\r
-            # in "data". This is the reason why we try twice.\r
-            #\r
-            # See also bug #282964.\r
-            #\r
-            # TODO: It might be faster to try to resolve the given name\r
-            # manually recursing the model metadata rather than\r
-            # constructing a queryset.\r
-            for lookup in (column.column.data, column.declared_name):\r
-                if not lookup or callable(lookup):\r
-                    continue\r
-                try:\r
-                    # Let Django validate the lookup by asking it to build\r
-                    # the final query; the way to do this has changed in\r
-                    # Django 1.2, and we try to support both versions.\r
-                    _temp = self.queryset.order_by(lookup).query\r
-                    if hasattr(_temp, 'as_sql'):\r
-                        _temp.as_sql()\r
-                    else:\r
-                        from django.db import DEFAULT_DB_ALIAS\r
-                        _temp.get_compiler(DEFAULT_DB_ALIAS).as_sql()\r
-                    break\r
-                except FieldError:\r
-                    pass\r
-            else:\r
-                return False\r
-\r
-        # if we haven't failed by now, the column should be valid\r
-        return True\r
-\r
-    def _build_snapshot(self):\r
-        """Overridden. The snapshot in this case is simply a queryset\r
-        with the necessary filters etc. attached.\r
-        """\r
-\r
-        # reset caches\r
-        self._columns._reset()\r
-        self._rows._reset()\r
-\r
-        queryset = self.queryset\r
-        if self.order_by:\r
-            actual_order_by = self._resolve_sort_directions(self.order_by)\r
-            queryset = queryset.order_by(*self._cols_to_fields(actual_order_by))\r
-        return queryset\r
+from django.core.exceptions import FieldError
+from django.utils.datastructures import SortedDict
+from base import BaseTable, DeclarativeColumnsMetaclass, \
+    Column, BoundRow, Rows, TableOptions, rmprefix, toggleprefix
+
+
+__all__ = ('ModelTable',)
+
+
+class ModelTableOptions(TableOptions):
+    def __init__(self, options=None):
+        super(ModelTableOptions, self).__init__(options)
+        self.model = getattr(options, 'model', None)
+        self.columns = getattr(options, 'columns', None)
+        self.exclude = getattr(options, 'exclude', None)
+
+
+def columns_for_model(model, columns=None, exclude=None):
+    """
+    Returns a ``SortedDict`` containing form columns for the given model.
+
+    ``columns`` is an optional list of field names. If provided, only the
+    named model fields will be included in the returned column list.
+
+    ``exclude`` is an optional list of field names. If provided, the named
+    model fields will be excluded from the returned list of columns, even
+    if they are listed in the ``fields`` argument.
+    """
+
+    field_list = []
+    opts = model._meta
+    for f in opts.fields + opts.many_to_many:
+        if (columns and not f.name in columns) or \
+           (exclude and f.name in exclude):
+            continue
+        column = Column() # TODO: chose correct column type, with right options
+        if column:
+            field_list.append((f.name, column))
+    field_dict = SortedDict(field_list)
+    if columns:
+        field_dict = SortedDict(
+            [(c, field_dict.get(c)) for c in columns
+                if ((not exclude) or (exclude and c not in exclude))]
+        )
+    return field_dict
+
+
+class BoundModelRow(BoundRow):
+    """Special version of the BoundRow class that can handle model instances
+    as data.
+
+    We could simply have ModelTable spawn the normal BoundRow objects
+    with the instance converted to a dict instead. However, this way allows
+    us to support non-field attributes and methods on the model as well.
+    """
+
+    def _default_render(self, boundcol):
+        """In the case of a model table, the accessor may use ``__`` to
+        span instances. We need to resolve this.
+        """
+        # try to resolve relationships spanning attributes
+        bits = boundcol.accessor.split('__')
+        current = self.data
+        for bit in bits:
+            # note the difference between the attribute being None and not
+            # existing at all; assume "value doesn't exist" in the former
+            # (e.g. a relationship has no value), raise error in the latter.
+            # a more proper solution perhaps would look at the model meta
+            # data instead to find out whether a relationship is valid; see
+            # also ``_validate_column_name``, where such a mechanism is
+            # already implemented).
+            if not hasattr(current, bit):
+                raise ValueError("Could not resolve %s from %s" % (bit, name))
+
+            current = getattr(current, bit)
+            if callable(current):
+                current = current()
+            # important that we break in None case, or a relationship
+            # spanning across a null-key will raise an exception in the
+            # next iteration, instead of defaulting.
+            if current is None:
+                break
+
+        if current is None:
+            # ...the whole name (i.e. the last bit) resulted in None
+            if boundcol.column.default is not None:
+                return boundcol.get_default(self)
+        return current
+
+
+class ModelRows(Rows):
+    row_class = BoundModelRow
+
+    def __init__(self, *args, **kwargs):
+        super(ModelRows, self).__init__(*args, **kwargs)
+
+    def _reset(self):
+        self._length = None
+
+    def __len__(self):
+        """Use the queryset count() method to get the length, instead of
+        loading all results into memory. This allows, for example,
+        smart paginators that use len() to perform better.
+        """
+        if getattr(self, '_length', None) is None:
+            self._length = self.table.data.count()
+        return self._length
+
+    # for compatibility with QuerySetPaginator
+    count = __len__
+
+
+class ModelTableMetaclass(DeclarativeColumnsMetaclass):
+    def __new__(cls, name, bases, attrs):
+        # Let the default form meta class get the declared columns; store
+        # those in a separate attribute so that ModelTable inheritance with
+        # differing models works as expected (the behaviour known from
+        # ModelForms).
+        self = super(ModelTableMetaclass, cls).__new__(
+            cls, name, bases, attrs, parent_cols_from='declared_columns')
+        self.declared_columns = self.base_columns
+
+        opts = self._meta = ModelTableOptions(getattr(self, 'Meta', None))
+        # if a model is defined, then build a list of default columns and
+        # let the declared columns override them.
+        if opts.model:
+            columns = columns_for_model(opts.model, opts.columns, opts.exclude)
+            columns.update(self.declared_columns)
+            self.base_columns = columns
+        return self
+
+
+class ModelTable(BaseTable):
+    """Table that is based on a model.
+
+    Similar to ModelForm, a column will automatically be created for all
+    the model's fields. You can modify this behaviour with a inner Meta
+    class:
+
+        class MyTable(ModelTable):
+            class Meta:
+                model = MyModel
+                exclude = ['fields', 'to', 'exclude']
+                columns = ['fields', 'to', 'include']
+
+    One difference to a normal table is the initial data argument. It can
+    be a queryset or a model (it's default manager will be used). If you
+    just don't any data at all, the model the table is based on will
+    provide it.
+    """
+
+    __metaclass__ = ModelTableMetaclass
+
+    rows_class = ModelRows
+
+    def __init__(self, data=None, *args, **kwargs):
+        if data == None:
+            if self._meta.model is None:
+                raise ValueError('Table without a model association needs '
+                    'to be initialized with data')
+            self.queryset = self._meta.model._default_manager.all()
+        elif hasattr(data, '_default_manager'): # saves us db.models import
+            self.queryset = data._default_manager.all()
+        else:
+            self.queryset = data
+
+        super(ModelTable, self).__init__(self.queryset, *args, **kwargs)
+
+    def _validate_column_name(self, name, purpose):
+        """Overridden. Only allow model-based fields and valid model
+        spanning relationships to be sorted."""
+
+        # let the base class sort out the easy ones
+        result = super(ModelTable, self)._validate_column_name(name, purpose)
+        if not result:
+            return False
+
+        if purpose == 'order_by':
+            column = self.columns[name]
+
+            # "data" can really be used in two different ways. It is
+            # slightly confusing and potentially should be changed.
+            # It can either refer to an attribute/field which the table
+            # column should represent, or can be a callable (or a string
+            # pointing to a callable attribute) that is used to render to
+            # cell. The difference is that in the latter case, there may
+            # still be an actual source model field behind the column,
+            # stored in "declared_name". In other words, we want to filter
+            # out column names that are not oderable, and the column name
+            # we need to check may either be stored in "data" or in
+            # "declared_name", depending on if and what kind of value is
+            # in "data". This is the reason why we try twice.
+            #
+            # See also bug #282964.
+            #
+            # TODO: It might be faster to try to resolve the given name
+            # manually recursing the model metadata rather than
+            # constructing a queryset.
+            for lookup in (column.column.data, column.declared_name):
+                if not lookup or callable(lookup):
+                    continue
+                try:
+                    # Let Django validate the lookup by asking it to build
+                    # the final query; the way to do this has changed in
+                    # Django 1.2, and we try to support both versions.
+                    _temp = self.queryset.order_by(lookup).query
+                    if hasattr(_temp, 'as_sql'):
+                        _temp.as_sql()
+                    else:
+                        from django.db import DEFAULT_DB_ALIAS
+                        _temp.get_compiler(DEFAULT_DB_ALIAS).as_sql()
+                    break
+                except FieldError:
+                    pass
+            else:
+                return False
+
+        # if we haven't failed by now, the column should be valid
+        return True
+
+    def _build_snapshot(self):
+        """Overridden. The snapshot in this case is simply a queryset
+        with the necessary filters etc. attached.
+        """
+
+        # reset caches
+        self._columns._reset()
+        self._rows._reset()
+
+        queryset = self.queryset
+        if self.order_by:
+            actual_order_by = self._resolve_sort_directions(self.order_by)
+            queryset = queryset.order_by(*self._cols_to_fields(actual_order_by))
+        return queryset