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