tables.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. import copy
  2. from collections import OrderedDict
  3. from itertools import count
  4. from django.conf import settings
  5. from django.core.paginator import Paginator
  6. from django.db import models
  7. from django.template.loader import get_template
  8. from django.utils.encoding import force_str
  9. from . import columns
  10. from .config import RequestConfig
  11. from .data import TableData
  12. from .rows import BoundRows
  13. from .utils import Accessor, AttributeDict, OrderBy, OrderByTuple, Sequence
  14. class DeclarativeColumnsMetaclass(type):
  15. """
  16. Metaclass that converts `.Column` objects defined on a class to the
  17. dictionary `.Table.base_columns`, taking into account parent class
  18. `base_columns` as well.
  19. """
  20. def __new__(mcs, name, bases, attrs):
  21. attrs["_meta"] = opts = TableOptions(attrs.get("Meta", None), name)
  22. # extract declared columns
  23. cols, remainder = [], {}
  24. for attr_name, attr in attrs.items():
  25. if isinstance(attr, columns.Column):
  26. attr._explicit = True
  27. cols.append((attr_name, attr))
  28. else:
  29. remainder[attr_name] = attr
  30. attrs = remainder
  31. cols.sort(key=lambda x: x[1].creation_counter)
  32. # If this class is subclassing other tables, add their fields as
  33. # well. Note that we loop over the bases in *reverse* - this is
  34. # necessary to preserve the correct order of columns.
  35. parent_columns = []
  36. for base in reversed(bases):
  37. if hasattr(base, "base_columns"):
  38. parent_columns = list(base.base_columns.items()) + parent_columns
  39. # Start with the parent columns
  40. base_columns = OrderedDict(parent_columns)
  41. # Possibly add some generated columns based on a model
  42. if opts.model:
  43. extra = OrderedDict()
  44. # honor Table.Meta.fields, fallback to model._meta.fields
  45. if opts.fields is not None:
  46. # Each item in opts.fields is the name of a model field or a normal attribute on the model
  47. for field_name in opts.fields:
  48. extra[field_name] = columns.library.column_for_field(
  49. field=Accessor(field_name).get_field(opts.model),
  50. accessor=field_name,
  51. linkify=opts.linkify.get(field_name),
  52. )
  53. else:
  54. for field in opts.model._meta.fields:
  55. extra[field.name] = columns.library.column_for_field(
  56. field, linkify=opts.linkify.get(field.name), accessor=field.name
  57. )
  58. # update base_columns with extra columns
  59. for key, column in extra.items():
  60. # skip current column because the parent was explicitly defined,
  61. # and the current column is not.
  62. if key in base_columns and base_columns[key]._explicit is True:
  63. continue
  64. base_columns[key] = column
  65. # Explicit columns override both parent and generated columns
  66. base_columns.update(OrderedDict(cols))
  67. # Apply any explicit exclude setting
  68. for exclusion in opts.exclude:
  69. if exclusion in base_columns:
  70. base_columns.pop(exclusion)
  71. # Remove any columns from our remainder, else columns from our parent class will remain
  72. for attr_name in remainder:
  73. if attr_name in base_columns:
  74. base_columns.pop(attr_name)
  75. # Set localize on columns
  76. for col_name in base_columns.keys():
  77. localize_column = None
  78. if col_name in opts.localize:
  79. localize_column = True
  80. # unlocalize gets higher precedence
  81. if col_name in opts.unlocalize:
  82. localize_column = False
  83. if localize_column is not None:
  84. base_columns[col_name].localize = localize_column
  85. attrs["base_columns"] = base_columns
  86. return super().__new__(mcs, name, bases, attrs)
  87. class TableOptions:
  88. """
  89. Extracts and exposes options for a `.Table` from a `.Table.Meta`
  90. when the table is defined. See `.Table` for documentation on the impact of
  91. variables in this class.
  92. Arguments:
  93. options (`.Table.Meta`): options for a table from `.Table.Meta`
  94. """
  95. def __init__(self, options, class_name):
  96. super().__init__()
  97. self._check_types(options, class_name)
  98. DJANGO_TABLES2_TEMPLATE = getattr(
  99. settings, "DJANGO_TABLES2_TEMPLATE", "django_tables2/table.html"
  100. )
  101. DJANGO_TABLES2_TABLE_ATTRS = getattr(settings, "DJANGO_TABLES2_TABLE_ATTRS", {})
  102. self.attrs = getattr(options, "attrs", DJANGO_TABLES2_TABLE_ATTRS)
  103. self.row_attrs = getattr(options, "row_attrs", {})
  104. self.pinned_row_attrs = getattr(options, "pinned_row_attrs", {})
  105. self.default = getattr(options, "default", "—")
  106. self.empty_text = getattr(options, "empty_text", None)
  107. self.fields = getattr(options, "fields", None)
  108. linkify = getattr(options, "linkify", [])
  109. if not isinstance(linkify, dict):
  110. linkify = dict.fromkeys(linkify, True)
  111. self.linkify = linkify
  112. self.exclude = getattr(options, "exclude", ())
  113. order_by = getattr(options, "order_by", None)
  114. if isinstance(order_by, str):
  115. order_by = (order_by,)
  116. self.order_by = OrderByTuple(order_by) if order_by is not None else None
  117. self.order_by_field = getattr(options, "order_by_field", "sort")
  118. self.page_field = getattr(options, "page_field", "page")
  119. self.per_page = getattr(options, "per_page", 25)
  120. self.per_page_field = getattr(options, "per_page_field", "per_page")
  121. self.prefix = getattr(options, "prefix", "")
  122. self.show_header = getattr(options, "show_header", True)
  123. self.show_footer = getattr(options, "show_footer", True)
  124. self.sequence = getattr(options, "sequence", ())
  125. self.orderable = getattr(options, "orderable", True)
  126. self.model = getattr(options, "model", None)
  127. self.template_name = getattr(options, "template_name", DJANGO_TABLES2_TEMPLATE)
  128. self.localize = getattr(options, "localize", ())
  129. self.unlocalize = getattr(options, "unlocalize", ())
  130. def _check_types(self, options, class_name):
  131. """
  132. Check class Meta attributes to prevent common mistakes.
  133. """
  134. if options is None:
  135. return
  136. checks = {
  137. (bool,): ["show_header", "show_footer", "orderable"],
  138. (int,): ["per_page"],
  139. (tuple, list, set): ["fields", "sequence", "exclude", "localize", "unlocalize"],
  140. (tuple, list, set, dict): ["linkify"],
  141. str: ["template_name", "prefix", "order_by_field", "page_field", "per_page_field"],
  142. (dict,): ["attrs", "row_attrs", "pinned_row_attrs"],
  143. (tuple, list, str): ["order_by"],
  144. (type(models.Model),): ["model"],
  145. }
  146. for types, keys in checks.items():
  147. for key in keys:
  148. value = getattr(options, key, None)
  149. if value is not None and not isinstance(value, types):
  150. expression = "{}.{} = {}".format(class_name, key, value.__repr__())
  151. raise TypeError(
  152. "{} (type {}), but type must be one of ({})".format(
  153. expression, type(value).__name__, ", ".join([t.__name__ for t in types])
  154. )
  155. )
  156. class Table(metaclass=DeclarativeColumnsMetaclass):
  157. """
  158. A representation of a table.
  159. Arguments:
  160. data (QuerySet, list of dicts): The data to display.
  161. This is a required variable, a `TypeError` will be raised if it's not passed.
  162. order_by: (tuple or str): The default ordering tuple or comma separated str.
  163. A hyphen `-` can be used to prefix a column name to indicate
  164. *descending* order, for example: `("name", "-age")` or `name,-age`.
  165. orderable (bool): Enable/disable column ordering on this table
  166. empty_text (str): Empty text to render when the table has no data.
  167. (default `.Table.Meta.empty_text`)
  168. exclude (iterable or str): The names of columns that should not be
  169. included in the table.
  170. attrs (dict): HTML attributes to add to the ``<table>`` tag.
  171. When accessing the attribute, the value is always returned as an
  172. `.AttributeDict` to allow easily conversion to HTML.
  173. row_attrs (dict): Add custom html attributes to the table rows.
  174. Allows custom HTML attributes to be specified which will be added
  175. to the ``<tr>`` tag of the rendered table.
  176. pinned_row_attrs (dict): Same as row_attrs but for pinned rows.
  177. sequence (iterable): The sequence/order of columns the columns (from
  178. left to right).
  179. Items in the sequence must be :term:`column names <column name>`, or
  180. `"..."` (string containing three periods). `'...'` can be used as a
  181. catch-all for columns that are not specified.
  182. prefix (str): A prefix for query string fields.
  183. To avoid name-clashes when using multiple tables on single page.
  184. order_by_field (str): If not `None`, defines the name of the *order by*
  185. query string field in the URL.
  186. page_field (str): If not `None`, defines the name of the *current page*
  187. query string field.
  188. per_page_field (str): If not `None`, defines the name of the *per page*
  189. query string field.
  190. template_name (str): The template to render when using ``{% render_table %}``
  191. (defaults to DJANGO_TABLES2_TEMPLATE, which is ``"django_tables2/table.html"``
  192. by default).
  193. default (str): Text to render in empty cells (determined by
  194. `.Column.empty_values`, default `.Table.Meta.default`)
  195. request: Django's request to avoid using `RequestConfig`
  196. show_header (bool): If `False`, the table will not have a header
  197. (`<thead>`), defaults to `True`
  198. show_footer (bool): If `False`, the table footer will not be rendered,
  199. even if some columns have a footer, defaults to `True`.
  200. extra_columns (str, `.Column`): list of `(name, column)`-tuples containing
  201. extra columns to add to the instance. If `column` is `None`, the column
  202. with `name` will be removed from the table.
  203. """
  204. def __init__(
  205. self,
  206. data=None,
  207. order_by=None,
  208. orderable=None,
  209. empty_text=None,
  210. exclude=None,
  211. attrs=None,
  212. row_attrs=None,
  213. pinned_row_attrs=None,
  214. sequence=None,
  215. prefix=None,
  216. order_by_field=None,
  217. page_field=None,
  218. per_page_field=None,
  219. template_name=None,
  220. default=None,
  221. request=None,
  222. show_header=None,
  223. show_footer=True,
  224. extra_columns=None,
  225. ):
  226. super().__init__()
  227. # note that although data is a keyword argument, it used to be positional
  228. # so it is assumed to be the first argument to this method.
  229. if data is None:
  230. raise TypeError("Argument data to {} is required".format(type(self).__name__))
  231. self.exclude = exclude or self._meta.exclude
  232. self.sequence = sequence
  233. self.data = TableData.from_data(data=data)
  234. self.data.set_table(self)
  235. if default is None:
  236. default = self._meta.default
  237. self.default = default
  238. # Pinned rows #406
  239. self.pinned_row_attrs = AttributeDict(pinned_row_attrs or self._meta.pinned_row_attrs)
  240. self.pinned_data = {
  241. "top": self.get_top_pinned_data(),
  242. "bottom": self.get_bottom_pinned_data(),
  243. }
  244. self.rows = BoundRows(data=self.data, table=self, pinned_data=self.pinned_data)
  245. self.attrs = AttributeDict(attrs if attrs is not None else self._meta.attrs)
  246. for tag in ["thead", "tbody", "tfoot"]:
  247. # Add these attrs even if they haven't been passed so we can safely refer to them in the templates
  248. self.attrs[tag] = AttributeDict(self.attrs.get(tag, {}))
  249. self.row_attrs = AttributeDict(row_attrs or self._meta.row_attrs)
  250. self.empty_text = empty_text if empty_text is not None else self._meta.empty_text
  251. self.orderable = orderable
  252. self.prefix = prefix
  253. self.order_by_field = order_by_field
  254. self.page_field = page_field
  255. self.per_page_field = per_page_field
  256. self.show_header = show_header
  257. self.show_footer = show_footer
  258. # Make a copy so that modifying this will not touch the class
  259. # definition. Note that this is different from forms, where the
  260. # copy is made available in a ``fields`` attribute.
  261. base_columns = copy.deepcopy(type(self).base_columns)
  262. if extra_columns is not None:
  263. for name, column in extra_columns:
  264. if column is None and name in base_columns:
  265. del base_columns[name]
  266. else:
  267. base_columns[name] = column
  268. # Keep fully expanded ``sequence`` at _sequence so it's easily accessible
  269. # during render. The priority is as follows:
  270. # 1. sequence passed in as an argument
  271. # 2. sequence declared in ``Meta``
  272. # 3. sequence defaults to '...'
  273. if sequence is not None:
  274. sequence = sequence
  275. elif self._meta.sequence:
  276. sequence = self._meta.sequence
  277. else:
  278. if self._meta.fields is not None:
  279. sequence = tuple(self._meta.fields) + ("...",)
  280. else:
  281. sequence = ("...",)
  282. sequence = Sequence(sequence)
  283. self._sequence = sequence.expand(base_columns.keys())
  284. # reorder columns based on sequence.
  285. base_columns = OrderedDict(((x, base_columns[x]) for x in sequence if x in base_columns))
  286. self.columns = columns.BoundColumns(self, base_columns)
  287. # `None` value for order_by means no order is specified. This means we
  288. # `shouldn't touch our data's ordering in any way. *However*
  289. # `table.order_by = None` means "remove any ordering from the data"
  290. # (it's equivalent to `table.order_by = ()`).
  291. if order_by is None and self._meta.order_by is not None:
  292. order_by = self._meta.order_by
  293. if order_by is None:
  294. self._order_by = None
  295. # If possible inspect the ordering on the data we were given and
  296. # update the table to reflect that.
  297. order_by = self.data.ordering
  298. if order_by is not None:
  299. self.order_by = order_by
  300. else:
  301. self.order_by = order_by
  302. self.template_name = template_name
  303. # If a request is passed, configure for request
  304. if request:
  305. RequestConfig(request).configure(self)
  306. self._counter = count()
  307. def get_top_pinned_data(self):
  308. """
  309. Return data for top pinned rows containing data for each row.
  310. Iterable type like: QuerySet, list of dicts, list of objects.
  311. Having a non-zero number of pinned rows
  312. will not result in an empty result set message being rendered,
  313. even if there are no regular data rows
  314. Returns:
  315. `None` (default) no pinned rows at the top, iterable, data for pinned rows at the top.
  316. Note:
  317. To show pinned row this method should be overridden.
  318. Example:
  319. >>> class TableWithTopPinnedRows(Table):
  320. ... def get_top_pinned_data(self):
  321. ... return [{
  322. ... "column_a" : "some value",
  323. ... "column_c" : "other value",
  324. ... }]
  325. """
  326. return None
  327. def get_bottom_pinned_data(self):
  328. """
  329. Return data for bottom pinned rows containing data for each row.
  330. Iterable type like: QuerySet, list of dicts, list of objects.
  331. Having a non-zero number of pinned rows
  332. will not result in an empty result set message being rendered,
  333. even if there are no regular data rows
  334. Returns:
  335. `None` (default) no pinned rows at the bottom, iterable, data for pinned rows at the bottom.
  336. Note:
  337. To show pinned row this method should be overridden.
  338. Example:
  339. >>> class TableWithBottomPinnedRows(Table):
  340. ... def get_bottom_pinned_data(self):
  341. ... return [{
  342. ... "column_a" : "some value",
  343. ... "column_c" : "other value",
  344. ... }]
  345. """
  346. return None
  347. def before_render(self, request):
  348. """
  349. A way to hook into the moment just before rendering the template.
  350. Can be used to hide a column.
  351. Arguments:
  352. request: contains the `WGSIRequest` instance, containing a `user` attribute if
  353. `.django.contrib.auth.middleware.AuthenticationMiddleware` is added to
  354. your `MIDDLEWARE_CLASSES`.
  355. Example::
  356. class Table(tables.Table):
  357. name = tables.Column(orderable=False)
  358. country = tables.Column(orderable=False)
  359. def before_render(self, request):
  360. if request.user.has_perm('foo.delete_bar'):
  361. self.columns.hide('country')
  362. else:
  363. self.columns.show('country')
  364. """
  365. return
  366. def as_html(self, request):
  367. """
  368. Render the table to an HTML table, adding `request` to the context.
  369. """
  370. # reset counter for new rendering
  371. self._counter = count()
  372. template = get_template(self.template_name)
  373. context = {"table": self, "request": request}
  374. self.before_render(request)
  375. return template.render(context)
  376. def as_values(self, exclude_columns=None):
  377. """
  378. Return a row iterator of the data which would be shown in the table where
  379. the first row is the table headers.
  380. arguments:
  381. exclude_columns (iterable): columns to exclude in the data iterator.
  382. This can be used to output the table data as CSV, excel, for example using the
  383. `~.export.ExportMixin`.
  384. If a column is defined using a :ref:`table.render_FOO`, the returned value from
  385. that method is used. If you want to differentiate between the rendered cell
  386. and a value, use a `value_Foo`-method::
  387. class Table(tables.Table):
  388. name = tables.Column()
  389. def render_name(self, value):
  390. return format_html('<span class="name">{}</span>', value)
  391. def value_name(self, value):
  392. return value
  393. will have a value wrapped in `<span>` in the rendered HTML, and just returns
  394. the value when `as_values()` is called.
  395. Note that any invisible columns will be part of the row iterator.
  396. """
  397. if exclude_columns is None:
  398. exclude_columns = ()
  399. columns = [
  400. column
  401. for column in self.columns.iterall()
  402. if not (column.column.exclude_from_export or column.name in exclude_columns)
  403. ]
  404. yield [force_str(column.header, strings_only=True) for column in columns]
  405. for row in self.rows:
  406. yield [
  407. force_str(row.get_cell_value(column.name), strings_only=True) for column in columns
  408. ]
  409. def has_footer(self):
  410. """
  411. Returns True if any of the columns define a ``_footer`` attribute or a
  412. ``render_footer()`` method
  413. """
  414. return self.show_footer and any(column.has_footer() for column in self.columns)
  415. @property
  416. def show_header(self):
  417. return self._show_header if self._show_header is not None else self._meta.show_header
  418. @show_header.setter
  419. def show_header(self, value):
  420. self._show_header = value
  421. @property
  422. def order_by(self):
  423. return self._order_by
  424. @order_by.setter
  425. def order_by(self, value):
  426. """
  427. Order the rows of the table based on columns.
  428. Arguments:
  429. value: iterable or comma separated string of order by aliases.
  430. """
  431. # collapse empty values to ()
  432. order_by = () if not value else value
  433. # accept string
  434. order_by = order_by.split(",") if isinstance(order_by, str) else order_by
  435. valid = []
  436. # everything's been converted to a iterable, accept iterable!
  437. for alias in order_by:
  438. name = OrderBy(alias).bare
  439. if name in self.columns and self.columns[name].orderable:
  440. valid.append(alias)
  441. self._order_by = OrderByTuple(valid)
  442. self.data.order_by(self._order_by)
  443. @property
  444. def order_by_field(self):
  445. return (
  446. self._order_by_field if self._order_by_field is not None else self._meta.order_by_field
  447. )
  448. @order_by_field.setter
  449. def order_by_field(self, value):
  450. self._order_by_field = value
  451. @property
  452. def page_field(self):
  453. return self._page_field if self._page_field is not None else self._meta.page_field
  454. @page_field.setter
  455. def page_field(self, value):
  456. self._page_field = value
  457. def paginate(self, paginator_class=Paginator, per_page=None, page=1, *args, **kwargs):
  458. """
  459. Paginates the table using a paginator and creates a ``page`` property
  460. containing information for the current page.
  461. Arguments:
  462. paginator_class (`~django.core.paginator.Paginator`): A paginator class to
  463. paginate the results.
  464. per_page (int): Number of records to display on each page.
  465. page (int): Page to display.
  466. Extra arguments are passed to the paginator.
  467. Pagination exceptions (`~django.core.paginator.EmptyPage` and
  468. `~django.core.paginator.PageNotAnInteger`) may be raised from this
  469. method and should be handled by the caller.
  470. """
  471. per_page = per_page or self._meta.per_page
  472. self.paginator = paginator_class(self.rows, per_page, *args, **kwargs)
  473. self.page = self.paginator.page(page)
  474. return self
  475. @property
  476. def per_page_field(self):
  477. return (
  478. self._per_page_field if self._per_page_field is not None else self._meta.per_page_field
  479. )
  480. @per_page_field.setter
  481. def per_page_field(self, value):
  482. self._per_page_field = value
  483. @property
  484. def prefix(self):
  485. return self._prefix if self._prefix is not None else self._meta.prefix
  486. @prefix.setter
  487. def prefix(self, value):
  488. self._prefix = value
  489. @property
  490. def prefixed_order_by_field(self):
  491. return "%s%s" % (self.prefix, self.order_by_field)
  492. @property
  493. def prefixed_page_field(self):
  494. return "%s%s" % (self.prefix, self.page_field)
  495. @property
  496. def prefixed_per_page_field(self):
  497. return "%s%s" % (self.prefix, self.per_page_field)
  498. @property
  499. def sequence(self):
  500. return self._sequence
  501. @sequence.setter
  502. def sequence(self, value):
  503. if value:
  504. value = Sequence(value)
  505. value.expand(self.base_columns.keys())
  506. self._sequence = value
  507. @property
  508. def orderable(self):
  509. if self._orderable is not None:
  510. return self._orderable
  511. else:
  512. return self._meta.orderable
  513. @orderable.setter
  514. def orderable(self, value):
  515. self._orderable = value
  516. @property
  517. def template_name(self):
  518. if self._template is not None:
  519. return self._template
  520. else:
  521. return self._meta.template_name
  522. @template_name.setter
  523. def template_name(self, value):
  524. self._template = value
  525. @property
  526. def paginated_rows(self):
  527. """
  528. Return the rows for the current page if the table is paginated, else all rows.
  529. """
  530. if hasattr(self, "page"):
  531. return self.page.object_list
  532. return self.rows
  533. def get_column_class_names(self, classes_set, bound_column):
  534. """
  535. Returns a set of HTML class names for cells (both ``td`` and ``th``) of a
  536. **bound column** in this table.
  537. By default this returns the column class names defined in the table's
  538. attributes.
  539. This method can be overridden to change the default behavior, for
  540. example to simply `return classes_set`.
  541. Arguments:
  542. classes_set(set of string): a set of class names to be added
  543. to the cell, retrieved from the column's attributes. In the case
  544. of a header cell (th), this also includes ordering classes.
  545. To set the classes for a column, see `.Column`.
  546. To configure ordering classes, see :ref:`ordering-class-name`
  547. bound_column(`.BoundColumn`): the bound column the class names are
  548. determined for. Useful for accessing `bound_column.name`.
  549. Returns:
  550. A set of class names to be added to cells of this column
  551. If you want to add the column names to the list of classes for a column,
  552. override this method in your custom table::
  553. class MyTable(tables.Table):
  554. ...
  555. def get_column_class_names(self, classes_set, bound_column):
  556. classes_set = super().get_column_class_names(classes_set, bound_column)
  557. classes_set.add(bound_column.name)
  558. return classes_set
  559. """
  560. return classes_set
  561. def table_factory(model, table=Table, fields=None, exclude=None, localize=None):
  562. """
  563. Return Table class for given `model`, equivalent to defining a custom table class::
  564. class MyTable(tables.Table):
  565. class Meta:
  566. model = model
  567. Arguments:
  568. model (`~django.db.models.Model`): Model associated with the new table
  569. table (`.Table`): Base Table class used to create the new one
  570. fields (list of str): Fields displayed in tables
  571. exclude (list of str): Fields exclude in tables
  572. localize (list of str): Fields to localize
  573. """
  574. attrs = {"model": model}
  575. if fields is not None:
  576. attrs["fields"] = fields
  577. if exclude is not None:
  578. attrs["exclude"] = exclude
  579. if localize is not None:
  580. attrs["localize"] = localize
  581. # If parent form class already has an inner Meta, the Meta we're
  582. # creating needs to inherit from the parent's inner meta.
  583. parent = (table.Meta, object) if hasattr(table, "Meta") else (object,)
  584. Meta = type("Meta", parent, attrs)
  585. # Give this new table class a reasonable name.
  586. class_name = model.__name__ + "AutogeneratedTable"
  587. # Class attributes for the new table class.
  588. table_class_attrs = {"Meta": Meta}
  589. return type(table)(class_name, (table,), table_class_attrs)