* Added pagination
[django-tables2.git] / django_tables / rows.py
1 # -*- coding: utf-8 -*-
2 from django.utils.safestring import EscapeUnicode, SafeData
3 from .proxies import TemplateSafeLazyProxy
4 import itertools
5
6
7 class BoundRow(object):
8     """
9     Represents a *specific* row in a table.
10
11     :class:`.BoundRow` objects are a container that make it easy to access the
12     final 'rendered' values for cells in a row. You can simply iterate over a
13     :class:`.BoundRow` object and it will take care to return values rendered
14     using the correct method (e.g. :meth:`.Column.render_FOO`)
15
16     To access the rendered value of each cell in a row, just iterate over it:
17
18     .. code-block:: python
19
20         >>> import django_tables as tables
21         >>> class SimpleTable(tables.Table):
22         ...     a = tables.Column()
23         ...     b = tables.CheckBoxColumn(attrs={'name': 'my_chkbox'})
24         ...
25         >>> table = SimpleTable([{'a': 1, 'b': 2}])
26         >>> row = table.rows[0]  # we only have one row, so let's use it
27         >>> for cell in row:
28         ...     print cell
29         ...
30         1
31         <input type="checkbox" name="my_chkbox" value="2" />
32
33     Alternatively you can treat it like a list and use indexing to retrieve a
34     specific cell. It should be noted that this will raise an IndexError on
35     failure.
36
37     .. code-block:: python
38
39         >>> row[0]
40         1
41         >>> row[1]
42         u'<input type="checkbox" name="my_chkbox" value="2" />'
43         >>> row[2]
44         ...
45         IndexError: list index out of range
46
47     Finally you can also treat it like a dictionary and use column names as the
48     keys. This will raise KeyError on failure (unlike the above indexing using
49     integers).
50
51     .. code-block:: python
52
53         >>> row['a']
54         1
55         >>> row['b']
56         u'<input type="checkbox" name="my_chkbox" value="2" />'
57         >>> row['c']
58         ...
59         KeyError: 'c'
60
61     :param table: is the :class:`Table` in which this row exists.
62     :param record: a single record from the :term:`table data` that is used to
63         populate the row. A record could be a :class:`Model` object, a
64         :class:`dict`, or something else.
65
66     """
67     def __init__(self, table, record):
68         self._table = table
69         self._record = record
70
71     @property
72     def table(self):
73         """The associated :class:`.Table` object."""
74         return self._table
75
76     @property
77     def record(self):
78         """
79         The data record from the data source which is used to populate this row
80         with data.
81
82         """
83         return self._record
84
85     def __iter__(self):
86         """
87         Iterate over the rendered values for cells in the row.
88
89         Under the hood this method just makes a call to
90         :meth:`.BoundRow.__getitem__` for each cell.
91
92         """
93         for column in self.table.columns:
94             # this uses __getitem__, using the name (rather than the accessor)
95             # is correct – it's what __getitem__ expects.
96             yield self[column.name]
97
98     def __getitem__(self, name):
99         """
100         Returns the final rendered value for a cell in the row, given the name
101         of a column.
102
103         """
104         bound_column = self.table.columns[name]
105
106         def value():
107             try:
108                 raw = bound_column.accessor.resolve(self.record)
109             except (TypeError, AttributeError, KeyError, ValueError) as e:
110                 raw = None
111             return raw if raw is not None else bound_column.default
112
113         kwargs = {
114             'value': TemplateSafeLazyProxy(value),
115             'record': self.record,
116             'column': bound_column.column,
117             'bound_column': bound_column,
118             'bound_row': self,
119             'table': self._table,
120         }
121         render_FOO = 'render_' + bound_column.name
122         render = getattr(self.table, render_FOO, bound_column.column.render)
123         try:
124             return render(**kwargs)
125         except TypeError as e:
126             # Let's be helpful and provide a decent error message, since
127             # render() underwent backwards incompatible changes.
128             if e.message.startswith('render() got an unexpected keyword'):
129                 if hasattr(self.table, render_FOO):
130                     cls = self.table.__class__.__name__
131                     meth = render_FOO
132                 else:
133                     cls = kwargs['column'].__class__.__name__
134                     meth = 'render'
135                 msg = 'Did you forget to add **kwargs to %s.%s() ?' % (cls, meth)
136                 raise TypeError(e.message + '. ' + msg)
137
138     def __contains__(self, item):
139         """Check by both row object and column name."""
140         if isinstance(item, basestring):
141             return item in self.table._columns
142         else:
143             return item in self
144
145
146 class BoundRows(object):
147     """
148     Container for spawning :class:`.BoundRow` objects.
149
150     The :attr:`.Table.rows` attribute is a :class:`.BoundRows` object.
151     It provides functionality that would not be possible with a simple iterator
152     in the table class.
153
154     :type table: :class:`.Table` object
155     :param table: the table in which the rows exist.
156
157     """
158     def __init__(self, table):
159         self.table = table
160
161     def all(self):
162         """
163         Return an iterable for all :class:`.BoundRow` objects in the table.
164
165         """
166         for record in self.table.data:
167             yield BoundRow(self.table, record)
168
169     def page(self):
170         """
171         If the table is paginated, return an iterable of :class:`.BoundRow`
172         objects that appear on the current page.
173
174         :rtype: iterable of :class:`.BoundRow` objects, or :const:`None`.
175         """
176         if not hasattr(self.table, 'page'):
177             return None
178         return iter(self.table.page.object_list)
179
180     def __iter__(self):
181         """Convience method for :meth:`.BoundRows.all`"""
182         return self.all()
183
184     def __len__(self):
185         """Returns the number of rows in the table."""
186         return len(self.table.data)
187
188     # for compatibility with QuerySetPaginator
189     count = __len__
190
191     def __getitem__(self, key):
192         """Allows normal list slicing syntax to be used."""
193         if isinstance(key, slice):
194             return itertools.imap(lambda record: BoundRow(self.table, record),
195                                   self.table.data[key])
196         elif isinstance(key, int):
197             return BoundRow(self.table, self.table.data[key])
198         else:
199             raise TypeError('Key must be a slice or integer.')