Make it slightly easier for custom table subclasses to customize the classes used...
authorMichael Elsdörfer <michael@elsdoerfer.info>
Wed, 24 Mar 2010 16:57:14 +0000 (17:57 +0100)
committerMichael Elsdörfer <michael@elsdoerfer.info>
Wed, 24 Mar 2010 16:57:14 +0000 (17:57 +0100)
Removed an unused _get_rows method from ModelTable.

django_tables/base.py
django_tables/models.py

index 37e5d569e4c29798a79eab6e1b23e8e2601cafe3..540ab637d0bc7939c5d6dc1ac9da277343aac1d2 100644 (file)
@@ -162,169 +162,6 @@ class OrderByTuple(tuple, StrAndUnicode):
             )\r
 \r
 \r
-class BaseTable(object):\r
-    """A collection of columns, plus their associated data rows.\r
-    """\r
-\r
-    __metaclass__ = DeclarativeColumnsMetaclass\r
-\r
-    def __init__(self, data, order_by=None):\r
-        """Create a new table instance with the iterable ``data``.\r
-\r
-        If ``order_by`` is specified, the data will be sorted accordingly.\r
-\r
-        Note that unlike a ``Form``, tables are always bound to data. Also\r
-        unlike a form, the ``columns`` attribute is read-only and returns\r
-        ``BoundColum`` wrappers, similar to the ``BoundField``'s you get\r
-        when iterating over a form. This is because the table iterator\r
-        already yields rows, and we need an attribute via which to expose\r
-        the (visible) set of (bound) columns - ``Table.columns`` is simply\r
-        the perfect fit for this. Instead, ``base_colums`` is copied to\r
-        table instances, so modifying that will not touch the class-wide\r
-        column list.\r
-        """\r
-        self._data = data\r
-        self._snapshot = None      # will store output dataset (ordered...)\r
-        self._rows = Rows(self)\r
-        self._columns = Columns(self)\r
-\r
-        self.order_by = order_by\r
-\r
-        # Make a copy so that modifying this will not touch the class\r
-        # definition. Note that this is different from forms, where the\r
-        # copy is made available in a ``fields`` attribute. See the\r
-        # ``Table`` class docstring for more information.\r
-        self.base_columns = copy.deepcopy(type(self).base_columns)\r
-\r
-    def _build_snapshot(self):\r
-        """Rebuild the table for the current set of options.\r
-\r
-        Whenver the table options change, e.g. say a new sort order,\r
-        this method will be asked to regenerate the actual table from\r
-        the linked data source.\r
-\r
-        Subclasses should override this.\r
-        """\r
-        self._snapshot = copy.copy(self._data)\r
-\r
-    def _get_data(self):\r
-        if self._snapshot is None:\r
-            self._build_snapshot()\r
-        return self._snapshot\r
-    data = property(lambda s: s._get_data())\r
-\r
-    def _resolve_sort_directions(self, order_by):\r
-        """Given an ``order_by`` tuple, this will toggle the hyphen-prefixes\r
-        according to each column's ``direction`` option, e.g. it translates\r
-        between the ascending/descending and the straight/reverse terminology.\r
-        """\r
-        result = []\r
-        for inst in order_by:\r
-            if self.columns[rmprefix(inst)].column.direction == Column.DESC:\r
-                inst = toggleprefix(inst)\r
-            result.append(inst)\r
-        return result\r
-\r
-    def _cols_to_fields(self, names):\r
-        """Utility function. Given a list of column names (as exposed to\r
-        the user), converts column names to the names we have to use to\r
-        retrieve a column's data from the source.\r
-\r
-        Usually, the name used in the table declaration is used for accessing\r
-        the source (while a column can define an alias-like name that will\r
-        be used to refer to it from the "outside"). However, a column can\r
-        override this by giving a specific source field name via ``data``.\r
-\r
-        Supports prefixed column names as used e.g. in order_by ("-field").\r
-        """\r
-        result = []\r
-        for ident in names:\r
-            # handle order prefix\r
-            if ident[:1] == '-':\r
-                name = ident[1:]\r
-                prefix = '-'\r
-            else:\r
-                name = ident\r
-                prefix = ''\r
-            # find the field name\r
-            column = self.columns[name]\r
-            if column.column.data and not callable(column.column.data):\r
-                name_in_source = column.column.data\r
-            else:\r
-                name_in_source = column.declared_name\r
-            result.append(prefix + name_in_source)\r
-        return result\r
-\r
-    def _validate_column_name(self, name, purpose):\r
-        """Return True/False, depending on whether the column ``name`` is\r
-        valid for ``purpose``. Used to validate things like ``order_by``\r
-        instructions.\r
-\r
-        Can be overridden by subclasses to impose further restrictions.\r
-        """\r
-        if purpose == 'order_by':\r
-            return name in self.columns and\\r
-                   self.columns[name].sortable\r
-        else:\r
-            return True\r
-\r
-    def _set_order_by(self, value):\r
-        if self._snapshot is not None:\r
-            self._snapshot = None\r
-        # accept both string and tuple instructions\r
-        order_by = (isinstance(value, basestring) \\r
-            and [value.split(',')] \\r
-            or [value])[0]\r
-        if order_by:\r
-            # validate, remove all invalid order instructions\r
-            validated_order_by = []\r
-            for o in order_by:\r
-                if self._validate_column_name(rmprefix(o), "order_by"):\r
-                    validated_order_by.append(o)\r
-                elif not options.IGNORE_INVALID_OPTIONS:\r
-                    raise ValueError('Column name %s is invalid.' % o)\r
-            self._order_by = OrderByTuple(validated_order_by)\r
-        else:\r
-            self._order_by = OrderByTuple()\r
-    order_by = property(lambda s: s._order_by, _set_order_by)\r
-\r
-    def __unicode__(self):\r
-        return self.as_html()\r
-\r
-    def __iter__(self):\r
-        for row in self.rows:\r
-            yield row\r
-\r
-    def __getitem__(self, key):\r
-        return self.rows[key]\r
-\r
-    # just to make those readonly\r
-    columns = property(lambda s: s._columns)\r
-    rows = property(lambda s: s._rows)\r
-\r
-    def as_html(self):\r
-        pass\r
-\r
-    def update(self):\r
-        """Update the table based on it's current options.\r
-\r
-        Normally, you won't have to call this method, since the table\r
-        updates itself (it's caches) automatically whenever you change\r
-        any of the properties. However, in some rare cases those\r
-        changes might not be picked up, for example if you manually\r
-        change ``base_columns`` or any of the columns in it.\r
-        """\r
-        self._build_snapshot()\r
-\r
-    def paginate(self, klass, *args, **kwargs):\r
-        page = kwargs.pop('page', 1)\r
-        self.paginator = klass(self.rows, *args, **kwargs)\r
-        try:\r
-            self.page = self.paginator.page(page)\r
-        except paginator.InvalidPage, e:\r
-            raise Http404(str(e))\r
-\r
-\r
 class Columns(object):\r
     """Container for spawning BoundColumns.\r
 \r
@@ -481,6 +318,50 @@ class BoundColumn(StrAndUnicode):
     def as_html(self):\r
         pass\r
 \r
+\r
+class BoundRow(object):\r
+    """Represents a single row of data, bound to a table.\r
+\r
+    Tables will spawn these row objects, wrapping around the actual data\r
+    stored in a row.\r
+    """\r
+    def __init__(self, table, data):\r
+        self.table = table\r
+        self.data = data\r
+\r
+    def __iter__(self):\r
+        for value in self.values:\r
+            yield value\r
+\r
+    def __getitem__(self, name):\r
+        """Returns this row's value for a column. All other access methods,\r
+        e.g. __iter__, lead ultimately to this."""\r
+\r
+        # We are supposed to return ``name``, but the column might be\r
+        # named differently in the source data.\r
+        result =  self.data[self.table._cols_to_fields([name])[0]]\r
+\r
+        # if the field we are pointing to is a callable, remove it\r
+        if callable(result):\r
+            result = result(self)\r
+        return result\r
+\r
+    def __contains__(self, item):\r
+        """Check by both row object and column name."""\r
+        if isinstance(item, basestring):\r
+            return item in self.table._columns\r
+        else:\r
+            return item in self\r
+\r
+    def _get_values(self):\r
+        for column in self.table.columns:\r
+            yield self[column.name]\r
+    values = property(_get_values)\r
+\r
+    def as_html(self):\r
+        pass\r
+\r
+\r
 class Rows(object):\r
     """Container for spawning BoundRows.\r
 \r
@@ -488,9 +369,11 @@ class Rows(object):
     provides functionality that would not be possible with a simple\r
     iterator in the table class.\r
     """\r
-    def __init__(self, table, row_klass=None):\r
+\r
+    row_class = BoundRow\r
+\r
+    def __init__(self, table):\r
         self.table = table\r
-        self.row_klass = row_klass and row_klass or BoundRow\r
 \r
     def _reset(self):\r
         pass   # we currently don't use a cache\r
@@ -498,7 +381,7 @@ class Rows(object):
     def all(self):\r
         """Return all rows."""\r
         for row in self.table.data:\r
-            yield self.row_klass(self.table, row)\r
+            yield self.row_class(self.table, row)\r
 \r
     def page(self):\r
         """Return rows on current page (if paginated)."""\r
@@ -516,51 +399,174 @@ class Rows(object):
         if isinstance(key, slice):\r
             result = list()\r
             for row in self.table.data[key]:\r
-                result.append(self.row_klass(self.table, row))\r
+                result.append(self.row_class(self.table, row))\r
             return result\r
         elif isinstance(key, int):\r
-            return self.row_klass(self.table, self.table.data[key])\r
+            return self.row_class(self.table, self.table.data[key])\r
         else:\r
             raise TypeError('Key must be a slice or integer.')\r
 \r
-class BoundRow(object):\r
-    """Represents a single row of data, bound to a table.\r
 \r
-    Tables will spawn these row objects, wrapping around the actual data\r
-    stored in a row.\r
+class BaseTable(object):\r
+    """A collection of columns, plus their associated data rows.\r
     """\r
-    def __init__(self, table, data):\r
-        self.table = table\r
-        self.data = data\r
 \r
-    def __iter__(self):\r
-        for value in self.values:\r
-            yield value\r
+    __metaclass__ = DeclarativeColumnsMetaclass\r
 \r
-    def __getitem__(self, name):\r
-        """Returns this row's value for a column. All other access methods,\r
-        e.g. __iter__, lead ultimately to this."""\r
+    rows_class = Rows\r
 \r
-        # We are supposed to return ``name``, but the column might be\r
-        # named differently in the source data.\r
-        result =  self.data[self.table._cols_to_fields([name])[0]]\r
+    def __init__(self, data, order_by=None):\r
+        """Create a new table instance with the iterable ``data``.\r
 \r
-        # if the field we are pointing to is a callable, remove it\r
-        if callable(result):\r
-            result = result(self)\r
+        If ``order_by`` is specified, the data will be sorted accordingly.\r
+\r
+        Note that unlike a ``Form``, tables are always bound to data. Also\r
+        unlike a form, the ``columns`` attribute is read-only and returns\r
+        ``BoundColum`` wrappers, similar to the ``BoundField``'s you get\r
+        when iterating over a form. This is because the table iterator\r
+        already yields rows, and we need an attribute via which to expose\r
+        the (visible) set of (bound) columns - ``Table.columns`` is simply\r
+        the perfect fit for this. Instead, ``base_colums`` is copied to\r
+        table instances, so modifying that will not touch the class-wide\r
+        column list.\r
+        """\r
+        self._data = data\r
+        self._snapshot = None      # will store output dataset (ordered...)\r
+        self._rows = self.rows_class(self)\r
+        self._columns = Columns(self)\r
+\r
+        self.order_by = order_by\r
+\r
+        # Make a copy so that modifying this will not touch the class\r
+        # definition. Note that this is different from forms, where the\r
+        # copy is made available in a ``fields`` attribute. See the\r
+        # ``Table`` class docstring for more information.\r
+        self.base_columns = copy.deepcopy(type(self).base_columns)\r
+\r
+    def _build_snapshot(self):\r
+        """Rebuild the table for the current set of options.\r
+\r
+        Whenver the table options change, e.g. say a new sort order,\r
+        this method will be asked to regenerate the actual table from\r
+        the linked data source.\r
+\r
+        Subclasses should override this.\r
+        """\r
+        self._snapshot = copy.copy(self._data)\r
+\r
+    def _get_data(self):\r
+        if self._snapshot is None:\r
+            self._build_snapshot()\r
+        return self._snapshot\r
+    data = property(lambda s: s._get_data())\r
+\r
+    def _resolve_sort_directions(self, order_by):\r
+        """Given an ``order_by`` tuple, this will toggle the hyphen-prefixes\r
+        according to each column's ``direction`` option, e.g. it translates\r
+        between the ascending/descending and the straight/reverse terminology.\r
+        """\r
+        result = []\r
+        for inst in order_by:\r
+            if self.columns[rmprefix(inst)].column.direction == Column.DESC:\r
+                inst = toggleprefix(inst)\r
+            result.append(inst)\r
         return result\r
 \r
-    def __contains__(self, item):\r
-        """Check by both row object and column name."""\r
-        if isinstance(item, basestring):\r
-            return item in self.table._columns\r
+    def _cols_to_fields(self, names):\r
+        """Utility function. Given a list of column names (as exposed to\r
+        the user), converts column names to the names we have to use to\r
+        retrieve a column's data from the source.\r
+\r
+        Usually, the name used in the table declaration is used for accessing\r
+        the source (while a column can define an alias-like name that will\r
+        be used to refer to it from the "outside"). However, a column can\r
+        override this by giving a specific source field name via ``data``.\r
+\r
+        Supports prefixed column names as used e.g. in order_by ("-field").\r
+        """\r
+        result = []\r
+        for ident in names:\r
+            # handle order prefix\r
+            if ident[:1] == '-':\r
+                name = ident[1:]\r
+                prefix = '-'\r
+            else:\r
+                name = ident\r
+                prefix = ''\r
+            # find the field name\r
+            column = self.columns[name]\r
+            if column.column.data and not callable(column.column.data):\r
+                name_in_source = column.column.data\r
+            else:\r
+                name_in_source = column.declared_name\r
+            result.append(prefix + name_in_source)\r
+        return result\r
+\r
+    def _validate_column_name(self, name, purpose):\r
+        """Return True/False, depending on whether the column ``name`` is\r
+        valid for ``purpose``. Used to validate things like ``order_by``\r
+        instructions.\r
+\r
+        Can be overridden by subclasses to impose further restrictions.\r
+        """\r
+        if purpose == 'order_by':\r
+            return name in self.columns and\\r
+                   self.columns[name].sortable\r
         else:\r
-            return item in self\r
+            return True\r
 \r
-    def _get_values(self):\r
-        for column in self.table.columns:\r
-            yield self[column.name]\r
-    values = property(_get_values)\r
+    def _set_order_by(self, value):\r
+        if self._snapshot is not None:\r
+            self._snapshot = None\r
+        # accept both string and tuple instructions\r
+        order_by = (isinstance(value, basestring) \\r
+            and [value.split(',')] \\r
+            or [value])[0]\r
+        if order_by:\r
+            # validate, remove all invalid order instructions\r
+            validated_order_by = []\r
+            for o in order_by:\r
+                if self._validate_column_name(rmprefix(o), "order_by"):\r
+                    validated_order_by.append(o)\r
+                elif not options.IGNORE_INVALID_OPTIONS:\r
+                    raise ValueError('Column name %s is invalid.' % o)\r
+            self._order_by = OrderByTuple(validated_order_by)\r
+        else:\r
+            self._order_by = OrderByTuple()\r
+    order_by = property(lambda s: s._order_by, _set_order_by)\r
+\r
+    def __unicode__(self):\r
+        return self.as_html()\r
+\r
+    def __iter__(self):\r
+        for row in self.rows:\r
+            yield row\r
+\r
+    def __getitem__(self, key):\r
+        return self.rows[key]\r
+\r
+    # just to make those readonly\r
+    columns = property(lambda s: s._columns)\r
+    rows = property(lambda s: s._rows)\r
 \r
     def as_html(self):\r
-        pass
\ No newline at end of file
+        pass\r
+\r
+    def update(self):\r
+        """Update the table based on it's current options.\r
+\r
+        Normally, you won't have to call this method, since the table\r
+        updates itself (it's caches) automatically whenever you change\r
+        any of the properties. However, in some rare cases those\r
+        changes might not be picked up, for example if you manually\r
+        change ``base_columns`` or any of the columns in it.\r
+        """\r
+        self._build_snapshot()\r
+\r
+    def paginate(self, klass, *args, **kwargs):\r
+        page = kwargs.pop('page', 1)\r
+        self.paginator = klass(self.rows, *args, **kwargs)\r
+        try:\r
+            self.page = self.paginator.page(page)\r
+        except paginator.InvalidPage, e:\r
+            raise Http404(str(e))\r
index a7cba827cdf7a00a5d40cff0e69f2e7785ccaa80..b7bf76cfbc29c5c88ddc78fda3e0522cd6173808 100644 (file)
@@ -38,6 +38,92 @@ def columns_for_model(model, columns=None, exclude=None):
             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
+    def __getitem__(self, name):\r
+        """Overridden. Return this row's data for a certain column, with\r
+        custom handling for model tables.\r
+        """\r
+\r
+        # find the column for the requested field, for reference\r
+        boundcol = self.table._columns[name]\r
+\r
+        # If the column has a name override (we know then that is was also\r
+        # used for access, e.g. if the condition is true, then\r
+        # ``boundcol.column.name == name``), we need to make sure we use the\r
+        # declaration name to access the model field.\r
+        if boundcol.column.data:\r
+            if callable(boundcol.column.data):\r
+                result = boundcol.column.data(self)\r
+                if not result:\r
+                    if boundcol.column.default is not None:\r
+                        return boundcol.get_default(self)\r
+                return result\r
+            else:\r
+                name = boundcol.column.data\r
+        else:\r
+            name = boundcol.declared_name\r
+\r
+\r
+        # try to resolve relationships spanning attributes\r
+        bits = name.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
@@ -79,6 +165,8 @@ class ModelTable(BaseTable):
 \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
@@ -91,7 +179,6 @@ class ModelTable(BaseTable):
             self.queryset = data\r
 \r
         super(ModelTable, self).__init__(self.queryset, *args, **kwargs)\r
-        self._rows = ModelRows(self)\r
 \r
     def _validate_column_name(self, name, purpose):\r
         """Overridden. Only allow model-based fields and valid model\r
@@ -159,90 +246,3 @@ class ModelTable(BaseTable):
             actual_order_by = self._resolve_sort_directions(self.order_by)\r
             queryset = queryset.order_by(*self._cols_to_fields(actual_order_by))\r
         self._snapshot = queryset\r
-\r
-    def _get_rows(self):\r
-        for row in self.data:\r
-            yield BoundModelRow(self, row)\r
-\r
-\r
-class ModelRows(Rows):\r
-    def __init__(self, *args, **kwargs):\r
-        super(ModelRows, self).__init__(*args, **kwargs)\r
-        self.row_klass = BoundModelRow\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
-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
-    def __getitem__(self, name):\r
-        """Overridden. Return this row's data for a certain column, with\r
-        custom handling for model tables.\r
-        """\r
-\r
-        # find the column for the requested field, for reference\r
-        boundcol = self.table._columns[name]\r
-\r
-        # If the column has a name override (we know then that is was also\r
-        # used for access, e.g. if the condition is true, then\r
-        # ``boundcol.column.name == name``), we need to make sure we use the\r
-        # declaration name to access the model field.\r
-        if boundcol.column.data:\r
-            if callable(boundcol.column.data):\r
-                result = boundcol.column.data(self)\r
-                if not result:\r
-                    if boundcol.column.default is not None:\r
-                        return boundcol.get_default(self)\r
-                return result\r
-            else:\r
-                name = boundcol.column.data\r
-        else:\r
-            name = boundcol.declared_name\r
-\r
-\r
-        # try to resolve relationships spanning attributes\r
-        bits = name.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
\ No newline at end of file