1from itertools import count
2
3from django.core.exceptions import ImproperlyConfigured
4from django.views.generic.list import ListView
5
6from . import tables
7from .config import RequestConfig
8
9
10class TableMixinBase:
11    """
12    Base mixin for the Single- and MultiTable class based views.
13    """
14
15    context_table_name = "table"
16    table_pagination = None
17
18    def get_context_table_name(self, table):
19        """
20        Get the name to use for the table's template variable.
21        """
22        return self.context_table_name
23
24    def get_table_pagination(self, table):
25        """
26        Return pagination options passed to `.RequestConfig`:
27            - True for standard pagination (default),
28            - False for no pagination,
29            - a dictionary for custom pagination.
30
31        `ListView`s pagination attributes are taken into account, if `table_pagination` does not
32        define the corresponding value.
33
34        Override this method to further customize pagination for a `View`.
35        """
36        paginate = self.table_pagination
37        if paginate is False:
38            return False
39
40        paginate = {}
41        if getattr(self, "paginate_by", None) is not None:
42            paginate["per_page"] = self.paginate_by
43        if hasattr(self, "paginator_class"):
44            paginate["paginator_class"] = self.paginator_class
45        if getattr(self, "paginate_orphans", 0) != 0:
46            paginate["orphans"] = self.paginate_orphans
47
48        # table_pagination overrides any MultipleObjectMixin attributes
49        if self.table_pagination:
50            paginate.update(self.table_pagination)
51
52        # we have no custom pagination settings, so just use the default.
53        if not paginate and self.table_pagination is None:
54            return True
55
56        return paginate
57
58
59class SingleTableMixin(TableMixinBase):
60    """
61    Adds a Table object to the context. Typically used with
62    `.TemplateResponseMixin`.
63
64    Attributes:
65        table_class: subclass of `.Table`
66        table_data: data used to populate the table, any compatible data source.
67        context_table_name(str): name of the table's template variable (default:
68            'table')
69        table_pagination (dict): controls table pagination. If a `dict`, passed as
70            the *paginate* keyword argument to `.RequestConfig`. As such, any
71            Truthy value enables pagination. (default: enable pagination).
72
73            The `dict` can be used to specify values for arguments for the call to
74            `~.tables.Table.paginate`.
75
76            If you want to use a non-standard paginator for example, you can add a key
77            `paginator_class` to the dict, containing a custom `Paginator` class.
78
79    This mixin plays nice with the Django's ``.MultipleObjectMixin`` by using
80    ``.get_queryset`` as a fall back for the table data source.
81    """
82
83    table_class = None
84    table_data = None
85
86    def get_table_class(self):
87        """
88        Return the class to use for the table.
89        """
90        if self.table_class:
91            return self.table_class
92        if self.model:
93            return tables.table_factory(self.model)
94
95        raise ImproperlyConfigured(
96            "You must either specify {0}.table_class or {0}.model".format(type(self).__name__)
97        )
98
99    def get_table(self, **kwargs):
100        """
101        Return a table object to use. The table has automatic support for
102        sorting and pagination.
103        """
104        table_class = self.get_table_class()
105        table = table_class(data=self.get_table_data(), **kwargs)
106        return RequestConfig(self.request, paginate=self.get_table_pagination(table)).configure(
107            table
108        )
109
110    def get_table_data(self):
111        """
112        Return the table data that should be used to populate the rows.
113        """
114        if self.table_data is not None:
115            return self.table_data
116        elif hasattr(self, "object_list"):
117            return self.object_list
118        elif hasattr(self, "get_queryset"):
119            return self.get_queryset()
120
121        klass = type(self).__name__
122        raise ImproperlyConfigured(
123            "Table data was not specified. Define {}.table_data".format(klass)
124        )
125
126    def get_table_kwargs(self):
127        """
128        Return the keyword arguments for instantiating the table.
129
130        Allows passing customized arguments to the table constructor, for example,
131        to remove the buttons column, you could define this method in your View::
132
133            def get_table_kwargs(self):
134                return {
135                    'exclude': ('buttons', )
136                }
137        """
138        return {}
139
140    def get_context_data(self, **kwargs):
141        """
142        Overridden version of `.TemplateResponseMixin` to inject the table into
143        the template's context.
144        """
145        context = super().get_context_data(**kwargs)
146        table = self.get_table(**self.get_table_kwargs())
147        context[self.get_context_table_name(table)] = table
148        return context
149
150
151class SingleTableView(SingleTableMixin, ListView):
152    """
153    Generic view that renders a template and passes in a `.Table` instances.
154
155    Mixes ``.SingleTableMixin`` with ``django.views.generic.list.ListView``.
156    """
157
158
159class MultiTableMixin(TableMixinBase):
160    """
161    Add a list with multiple Table object's to the context. Typically used with
162    `.TemplateResponseMixin`.
163
164    The `tables` attribute must be either a list of `.Table` instances or
165    classes extended from `.Table` which are not already instantiated. In that
166    case, `get_tables_data` must be able to return the tables data, either by
167    having an entry containing the data for each table in `tables`, or by
168    overriding this method in order to return this data.
169
170    Attributes:
171        tables: list of `.Table` instances or list of `.Table` child objects.
172        tables_data: if defined, `tables` is assumed to be a list of table
173            classes which will be instantiated with the corresponding item from
174            this list of `.TableData` instances.
175        table_prefix(str): Prefix to be used for each table. The string must
176            contain one instance of `{}`, which will be replaced by an integer
177            different for each table in the view. Default is 'table_{}-'.
178        context_table_name(str): name of the table's template variable (default:
179            'tables')
180
181    .. versionadded:: 1.2.3
182    """
183
184    tables = None
185    tables_data = None
186
187    table_prefix = "table_{}-"
188
189    # override context table name to make sense in a multiple table context
190    context_table_name = "tables"
191
192    def get_tables(self):
193        """
194        Return an array of table instances containing data.
195        """
196        if self.tables is None:
197            klass = type(self).__name__
198            raise ImproperlyConfigured("No tables were specified. Define {}.tables".format(klass))
199        data = self.get_tables_data()
200
201        if data is None:
202            return self.tables
203
204        if len(data) != len(self.tables):
205            klass = type(self).__name__
206            raise ImproperlyConfigured("len({}.tables_data) != len({}.tables)".format(klass, klass))
207        return list(Table(data[i]) for i, Table in enumerate(self.tables))
208
209    def get_tables_data(self):
210        """
211        Return an array of table_data that should be used to populate each table
212        """
213        return self.tables_data
214
215    def get_context_data(self, **kwargs):
216        context = super().get_context_data(**kwargs)
217        tables = self.get_tables()
218
219        # apply prefixes and execute requestConfig for each table
220        table_counter = count()
221        for table in tables:
222            table.prefix = table.prefix or self.table_prefix.format(next(table_counter))
223
224            RequestConfig(self.request, paginate=self.get_table_pagination(table)).configure(table)
225
226            context[self.get_context_table_name(table)] = list(tables)
227
228        return context
229