django-tables2-2.1.1/0000755000175000017500000000000013542204315013650 5ustar josephjosephdjango-tables2-2.1.1/requirements/0000755000175000017500000000000013542204315016373 5ustar josephjosephdjango-tables2-2.1.1/requirements/common.pip0000644000175000017500000000016113542204315020373 0ustar josephjoseph# mocks/stubs for tests fudge # xml parsing lxml pytz>0 tablib==0.13.0 mock psycopg2-binary django-filter==2.2.0 django-tables2-2.1.1/requirements/django-dev.pip0000644000175000017500000000003413542204315021120 0ustar josephjoseph-r common.pip Django==2.2.3 django-tables2-2.1.1/CHANGELOG.md0000644000175000017500000010647513542204315015476 0ustar josephjoseph# Change log ## 2.1.1 (2019-09-23) - Made `ManyToManyColumn` use `table.default` instead of a local value [#680](https://github.com/jieter/django-tables2/pull/680) by [@srtab](https://github.com/srtab) - Removed invalid scope attribute in `` element of `bootstrap4.html`. [#691](https://github.com/jieter/django-tables2/pull/691) by [@vlt](https://github.com/vlt) - Fixed an issue with incorrectly disabled pagination where `SingleTableMixin` was not used together with `ListView` [#678](https://github.com/jieter/django-tables2/pull/678) by [@nieuwenhuys](https://github.com/nieuwenhuys) ## 2.1.0 (2019-07-22) - Dropped support for python 2.7 (and django 1.11). - Removed `django_tables2.utils.ucfirst`, use `django.utils.text.capfirst` instead. - Removed `class="thead-default"` from bootstrap4 template ([#671](https://github.com/jieter/django-tables2/issues/671)) - Included columns with `visible=False` in export ([#677](https://github.com/jieter/django-tables2/pull/677)) - Fixed pagination when the number of pages is equal to page range plus one ([#655](https://github.com/jieter/django-tables2/pull/655)) ## 2.0.6 (2019-03-26) - Add optional 'table' kwarg to `row_attrs` callables ## 2.0.5 (2019-02-21) - Fixes issue with wrong time format for TimeColumn [#650](https://github.com/jieter/django-tables2/pull/650) by [@IgorCode](https://github.com/IgorCode) ## 2.0.4 (2019-01-21) - The `ValueError` raised if the QuerySet passed to a table instance did not match the value declared in `Meta.model` is now turned into a warning (fixes [#643](https://github.com/jieter/django-tables2/issues/643)) - Make sure the templates do not raise errors when `thead`/`tfoot` attributes are not defined [#624](https://github.com/jieter/django-tables2/pull/624) by [@intiocean](https://github.com/intiocean) ## 2.0.3 (2018-11-11) - Improvements in packaging and publishing helper scripts reducing the package size considerably [#630](https://github.com/jieter/django-tables2/pull/630) by [@wtayyeb](https://github.com/wtayyeb) (fixes [#629](https://github.com/jieter/django-tables2/issues/629)) - Documentation improvements fixing [#625](https://github.com/jieter/django-tables2/issues/625), [#631](https://github.com/jieter/django-tables2/issues/631) ## 2.0.2 (2018-10-22) - Make sure the value of the class attribute in `` has consistent ordering (fixes [#627](https://github.com/jieter/django-tables2/issues/627)) - Make sure that pagination block is available in template regardless of pagination status [#622](https://github.com/jieter/django-tables2/pull/622) by [@apocalyptech](https://github.com/apocalyptech) ## 2.0.1 (2018-09-13) - Fixed a regression which did not allow `Table.Meta.order_by` to be a list. ## 2.0.0 (2018-09-13) Not much changed in this final version, but quite a lot if you are still on 1.21.2. Some [breaking changes](#breaking-changes-200) were introduced in version 2.0.0a0, so before upgrading from 1.21.2, please have a look through them carefully. - Consider `ExportMixin.export_trigger_param` in `export_url` template tag [#609](https://github.com/jieter/django-tables2/pull/609) by [@soerenbe](https://github.com/soerenbe) ## 2.0.0b5 (2018-08-29) - Change order of logic in `get_table_pagination` to make sure we are able to override the paginator using `View.paginator_class` attribute. ## 2.0.0b4 (2018-08-29) - The `klass` argument to `Table.paginate()` is renamed to `paginator_class` - Table views/mixins now take `ListView` attributes `paginator_class` and `paginate_orphans` into account. ## 2.0.0b3 (2018-08-27) - Fixed a bug in the implementation of [#606](https://github.com/jieter/django-tables2/pull/606) ## 2.0.0b2 (2018-08-27) - Added the ability to change the html attributes for `thead`, `tbody`, `tfoot` tags [#606](https://github.com/jieter/django-tables2/pull/606) by [@roelbouwman](https://github.com/roelbouwman) ## 2.0.0b1 (2018-08-24) - Added `LazyPaginator` to prevent making `.count()` queries ([#604](https://github.com/jieter/django-tables2/pull/604)). ## 2.0.0a5 (2018-07-28) - Added `linkify_item` keyword argument to `ManyToManyColumn`, fixes [#594](https://github.com/jieter/django-tables2/issues/594) - Fixed an encoding issue in `README.md` preventing installation in some environments. ## 2.0.0a4 (2018-07-17) - Add `linkify` keyword argument to all columns, to allow wrapping the content in a `` tag. It accepts one of these ways to define the link: - `True` to use the record return value of `record.get_absolute_url()`, - a callable to use its return value - a dict which is passed on to `django.urls.reverse()` - a (viewname, args) or (viewname, kwargs)-tuple which is also passed on to `django.urls.reverse()`. Implementation should be backwards compatible, so all use of `LinkColumn` and `RelatedLinkColum` should still work. [#590](https://github.com/jieter/django-tables2/pull/590) ## 2.0.0a3 (2018-05-24) Hello from [DjangoCon Europe](https://2018.djangocon.eu/)! - Fix table prefix being overwritten in `MultiTableView`, [#576](https://github.com/jieter/django-tables2/pull/576) by [@ETinLV](https://github.com/ETinLV), (fixes [#572](https://github.com/jieter/django-tables2/issues/572)) - Fix `empty_text` cannot be translated (fixes [#579](https://github.com/jieter/django-tables2/issues/579)) ## 2.0.0a2 (2018-04-13) - Another round of template cleanup. - Fresh screenshots - Prevent crash in `RelatedLinkColumn` for records without `get_absolute_url()`. - Raise `ValueError` when `Table.Meta.model != QuerySet.Model`. - Raise `TypeError` when incorrect types are used for `Table.Meta` attributes (fixes [#517](https://github.com/jieter/django-tables2/issues/517)) - Fix: `Table.Meta.sequence` with `extra_columns` can leads to `KeyError` (fixes [#486](https://github.com/jieter/django-tables2/issues/486)) ## 2.0.0a1 (2018-04-12) - Fixed translation of 'previous' for some languages (fixes [#563](https://github.com/jieter/django-tables2/issues/563)) ## django-tables2 2.0.0a0 (2018-04-10) - Cleaned up templates to add consistency in what is presented across all templates. - Added bootstrap4.html template - Fixed translation inconsistencies. ### breaking changes 2.0.0 - Appearance of the paginators might be different from the current 1.x templates. Use a custom template if you need to keep the appearance the same. - Removed the `template` argument to the table constructor, use `template_name` instead. - Stopped adding column names to the class attribute of table cells (`` tags) by default. Previous behavior can be restored by using this method on your custom table: ```python class MyTable(tables.Table): # columns def get_column_class_names(self, classes_set, bound_column): classes_set = super(MyTable, self).get_column_class_names(classes_set, bound_column) classes_set.add(bound_column.name) return classes_set ``` - `verbose_name`s derived from model fields are not passed through `title()` anymore, only the first character is converted to upper case. This follows [Django's convention for verbose field names](https://docs.djangoproject.com/en/2.0/topics/db/models/#verbose-field-names): "The convention is not to capitalize the first letter of the verbose_name. Django will automatically capitalize the first letter where it needs to." (Fixes [#475](https://github.com/jieter/django-tables2/issues/475) and [#491](https://github.com/jieter/django-tables2/issues/491)) ## 1.21.2 (2018-03-26) - Moved table instantiation from `get_context_data` to `get_tables` [#554](https://github.com/jieter/django-tables2/pull/554) by [@sdolemelipone](https://github.com/sdolemelipone) - Pass request as kwarg to `template.render()`, rather than as part of context. (fixes [#552](https://github.com/jieter/django-tables2/issues/552)) ## 1.21.1 (2018-03-12) - Do not perform extra `COUNT()` queries for non-paginated tables. Fixes [#551](https://github.com/jieter/django-tables2/issues/551) ## 1.21.0 (2018-03-12) - Add new method `paginated_rows` to `Table` to replace fallback to non-paginated rows in templates. - Prevent mutation of the template context `{% render_table %}` is called from (fixes [#547](https://github.com/jieter/django-tables2/issues/547)) **Possible breaking change**: the context variables of the template `{% render_table %}` is called from is no longer available in the table's template. The `table` variable has an attribute `context`, which is the context of the calling template. Use `{{ table.context.variable }}` instead of `{{ variable }}`. ## 1.20.0 (2018-03-08) - Define and use `get_table_data` in `MultiTableMixin` [#538](https://github.com/jieter/django-tables2/pull/538) by [@vCra](https://github.com/vCra) (fixes [#528](https://github.com/jieter/django-tables2/issues/528)) - Added `{% export_url %}` template tag. - Allow passing a `TableData`-derived class to the data argument of the `Table` constructor, instead of a QuerySet or list of dicts. ## 1.19.0 (2018-02-02) - `BoundColumn.attrs` does not evaluate `current_value` as `bool` [#536](https://github.com/jieter/django-tables2/pull/536) by [@pachewise](https://github.com/pachewise) (fixes [#534](https://github.com/jieter/django-tables2/issues/534)) - Allow more flexible access to cell values (especially useful for django templates) (fixes [#485](https://github.com/jieter/django-tables2/issues/485)) ## 1.18.0 (2018-01-27) - Follow relations when detecting column type for fields in `Table.Meta.fields` (fixes [#498](https://github.com/jieter/django-tables2/issues/498)) - Renamed `Table.Meta.template` to `template_name` (with deprecation warning for the former) [#542](https://github.com/jieter/django-tables2/pull/524) (fixes [#520](https://github.com/jieter/django-tables2/issues/520)) - Added Czech translation [#533](https://github.com/jieter/django-tables2/pull/533) by [@OndraRehounek](https://github.com/OndraRehounek) - Added `table_factory` [#532](https://github.com/jieter/django-tables2/pull/532) by [@ZuluPro](https://github.com/ZuluPro) ## 1.17.1 (2017-12-14) - Fix typo in setup.py for `extras_require`. ## 1.17.0 (2017-12-14) - Dropped support for Django 1.8, 1.9 and 1.10. - Add `extra_context` argument to `TemplateColumn` [#509](https://github.com/jieter/django-tables2/pull/509) by [@ad-m](https://github.com/ad-m) - Remove unnecessary cast of record to `str` [#514](https://github.com/jieter/django-tables2/pull/514), fixes [#511](https://github.com/jieter/django-tables2/issues/511) - Use `django.test.TestCase` for all tests, and remove dependency on pytest and reorganized some tests [#515](https://github.com/jieter/django-tables2/pull/515) - Remove traces of django-haystack tests from the tests, there were no actual tests. ## 1.16.0 (2017-11-27) This is the last version supporting Django 1.8, 1.9 and 1.10. Django 1.8 is only supported until April 2018, so consider upgrading to Django 1.11! - Added `tf` dictionary to `Column.attrs` with default values for the footer, so footers now have `class` attribute by default [#501](https://github.com/jieter/django-tables2/pull/501) by [@mpasternak](https://github.com/mpasternak) ## 1.15.0 (2017-11-23) - Added `as=varname` keyword argument to the `{% querystring %}` template tag, fixes [#481](https://github.com/jieter/django-tables2/issues/481) - Updated the tutorial to reflect current state of Django a bit better. - Used `OrderedDict` rather than `dict` as the parent for `utils.AttributeDict` to make the rendered html more consistent across python versions. - Allow reading column `attrs` from a column's attribute, allowing easier reuse of custom column attributes (fixes [#241](https://github.com/jieter/django-tables2/issues/241)) - `value` and `record` are optionally passed to the column attrs callables for data rows. [#503](https://github.com/jieter/django-tables2/pull/503), fixes [#500](https://github.com/jieter/django-tables2/issues/500) ## 1.14.2 (2017-10-30) - Added a `row_counter` variable to the template context in `TemplateColumn` (fixes [#448](https://github.com/jieter/django-tables2/issues/488)) ## 1.14.1 (2017-10-30) - Do not fail if `orderable=False` is passed to `ManyToManyColumn()` ## 1.14.0 (2017-10-30) - Added `separator` argument to `ManyToManyColumn`. - Allow `mark_safe()`'d strings from `ManyToManyColumn.tranform()` - Disabled ordering on `ManyToManyColumns` by default. ## 1.13.0 (2017-10-17) - Made positional `data` argument to the table `__init__()` a keyword argument to make inheritance easier. Will raise a `TypeError` if omitted. ## 1.12.0 (2017-10-10) - Allow export file name customization [#484](https://github.com/bradleyayers/django-tables2/pull/484) by [@federicobond](https://github.com/federicobond) - Fixed a bug where template columns were not rendered for pinned rows ([#483](https://github.com/bradleyayers/django-tables2/pull/483) by [@khirstinova](https://github.com/khirstinova), fixes [#482](https://github.com/bradleyayers/django-tables2/issues/482)) ## 1.11.0 (2017-09-15) - Added Hungarian translation [#471](https://github.com/bradleyayers/django-tables2/pull/471) by [@hmikihth](https://github.com/hmikihth). - Added TemplateColumn.value() and enhanced export docs (fixes [#470](https://github.com/bradleyayers/django-tables2/issues/470)) - Fixed display of pinned rows if table has no data. [#477](https://github.com/bradleyayers/django-tables2/pull/477) by [@khirstinova](https://github.com/khirstinova) ## 1.10.0 (2017-06-30) - Added `ManyToManyColumn` automatically added for `ManyToManyField`s. ## 1.9.1 (2017-06-29) - Allow customizing the value used in `Table.as_values()` (when using a `render_` method) using a `value_` method. (fixes [#458](https://github.com/bradleyayers/django-tables2/issues/458)) - Allow excluding columns from the `Table.as_values()` output. (fixes [#459](https://github.com/bradleyayers/django-tables2/issues/459)) - Fixed unicode handling for column headers in `Table.as_values()` ## 1.9.0 (2017-06-22) - Allow computable attrs for ``-tags from `Table.attrs` ([#457](https://github.com/bradleyayers/django-tables2/pull/457), fixes [#451](https://github.com/bradleyayers/django-tables2/issues/451)) ## 1.8.0 (2017-06-17) - Feature: Added an `ExportMixin` to export table data in various export formats (CSV, XLS, etc.) using [tablib](http://docs.python-tablib.org/en/latest/). - Defer expanding `Meta.sequence` to `Table.__init__`, to make sequence work in combination with `extra_columns` (fixes [#450](https://github.com/bradleyayers/django-tables2/issues/450)) - Fixed a crash when `MultiTableMixin.get_tables()` returned an empty array ([#454](https://github.com/bradleyayers/django-tables2/pull/455) by [@pypetey](https://github.com/pypetey) ## 1.7.1 (2017-06-02) - Call before_render when rendering with the render_table template tag (fixes [#447](https://github.com/bradleyayers/django-tables2/issues/447)) ## 1.7.0 (2017-06-01) - Make `title()` lazy ([#443](https://github.com/bradleyayers/django-tables2/pull/443) by [@ygwain](https://github.com/ygwain), fixes [#438](https://github.com/bradleyayers/django-tables2/issues/438)) - Fix `__all__` by populating them with the names of the items to export instead of the items themselves. - Allow adding extra columns to an instance using the `extra_columns` argument. Fixes [#403](https://github.com/bradleyayers/django-tables2/issues/403), [#70](https://github.com/bradleyayers/django-tables2/issues/70) - Added a hook `before_render` to allow last-minute changes to the table before rendering. - Added `BoundColumns.show()` and `BoundColumns.hide()` to show/hide columns on an instance of a `Table`. - Use `.verbose_name`/`.verbose_name_plural` if it exists to name the items in the list. (fixes [#166](https://github.com/bradleyayers/django-tables2/issues/166)) ## 1.6.1 (2017-05-08) - Add missing pagination to the responsive bootstrap template ([#440](https://github.com/bradleyayers/django-tables2/pull/440) by [@tobiasmcnulty](https://github.com/tobiasmcnulty)) ## 1.6.0 (2017-05-01) - Add new template `bootstrap-responsive.html` to generate a responsive bootstrap table. (Fixes [#436](https://github.com/bradleyayers/django-tables2/issues/436)) ## 1.5.0 (2017-04-18) _Full disclosure: as of april 1st, 2017, I am an employee of [Zostera](http://zostera.nl/), as such I will continue to maintain and improve django-tables2._ - Made `TableBase.as_values()` an iterator ([#432](https://github.com/bradleyayers/django-tables2/pull/432) by [@pziarsolo](https://github.com/pziarsolo)) - Added `JSONField` for data in JSON format. - Added `__all__` in `django_tables2/__init__.py` and `django_tables2/columns/__init__.py` - Added a setting `DJANGO_TABLES2_TEMPLATE` to allow project-wide overriding of the template used to render tables (fixes [#434](https://github.com/bradleyayers/django-tables2/issues/434)). ## 1.4.2 (2017-03-06) - Feature: Pinned rows ([#411](https://github.com/bradleyayers/django-tables2/pull/411) by [@djk2](https://github.com/djk2), fixes [#406](https://github.com/bradleyayers/django-tables2/issues/406)) - Fix an issue where `ValueError` was raised while using a view with a `get_queryset()` method defined. (fix with [#423](https://github.com/bradleyayers/django-tables2/pull/423) by [@desecho](https://github.com/desecho)) ## 1.4.1 (2017-02-27) - Fix URLS to screenshots in on PyPi description (fixes [ #398](https://github.com/bradleyayers/django-tables2/issues/398)) - Prevent superfluous spaces when a callable `row_attrs['class']` returns an empty string ([#417](https://github.com/bradleyayers/django-tables2/pull/417) by [@Superman8218](https://github.com/Superman8218)), fixes [#416](https://github.com/bradleyayers/django-tables2/issues/416)) ## 1.4.0 (2017-02-27) - Return `None` from `Table.as_values()` for missing values. [#419](https://github.com/bradleyayers/django-tables2/pull/419) - Fix ordering by custom fields, and refactor `TableData` [#424](https://github.com/bradleyayers/django-tables2/pull/424), fixes [#413](https://github.com/bradleyayers/django-tables2/issues/413) - Revert removing `TableData.__iter__()` (removed in [this commit](https://github.com/bradleyayers/django-tables2/commit/8fe9826429e6945a9258bc181fcbd711b282dba9)), fixes [#427](https://github.com/bradleyayers/django-tables2/issues/427), [#361](https://github.com/bradleyayers/django-tables2/issues/361) and [#421](https://github.com/bradleyayers/django-tables2/issues/421). ## 1.3.0 (2017-01-20) - Implement method `Table.as_values()` to get it's raw values. [#394](https://github.com/bradleyayers/django-tables2/pull/394) by [@intiocean](https://github.com/intiocean) - Fix some compatibility issues with django 2.0 [#408](https://github.com/bradleyayers/django-tables2/pull/409) by [djk2](https://github.com/djk2) ## 1.2.9 (2016-12-21) - Documentation for `None`-column attributes [#401](https://github.com/bradleyayers/django-tables2/pull/401) by [@dyve](https://github.com/dyve) ## 1.2.8 (2016-12-21) - `None`-column attributes on child class overwrite column attributes of parent class [#400](https://github.com/bradleyayers/django-tables2/pull/400) by [@dyve](https://github.com/dyve) ## 1.2.7 (2016-12-12) - Apply `title` to a column's `verbose_name` when it is derived from a model, fixes [#249](https://github.com/bradleyayers/django-tables2/issues/249). ([#382](https://github.com/bradleyayers/django-tables2/pull/382) by [@shawnnapora](https://github.com/shawnnapora)) - Update documentation after deprecation of `STATIC_URL` in django ([#384](https://github.com/bradleyayers/django-tables2/pull/384), by [@velaia](https://github.com/velaia)) - Cleanup of the templates, making the output more equal ([#381](https://github.com/bradleyayers/django-tables2/pull/381) by [@ralgozino](https://github.com/ralgozino)) - Use new location for `urlresolvers` in Django and add backwards compatible import ([#388](https://github.com/bradleyayers/django-tables2/pull/388) by [@felixxm](https://github.com/felixxm)) - Fix a bug where using `sequence` and then `exclude` in a child table would result in a `KeyError` - Some documentation fixes and cleanups. ## 1.2.6 (2016-09-06) - Added `get_table_kwargs()` method to `SingleTableMixin` to allow passing custom keyword arguments to the `Table` constructor. ([#366](https://github.com/bradleyayers/django-tables2/pull/366) by [@fritz-k](https://github.com/fritz-k)) - Allow the children of `TableBase` render in the `{% render_table %}` template tag. ([#377](https://github.com/bradleyayers/django-tables2/pull/377) by [@shawnnapora](https://github.com/shawnnapora)) - Refactor `BoundColumn` attributes to allow override of CSS class names, fixes [#349](https://github.com/bradleyayers/django-tables2/issues/349) ([#370](https://github.com/bradleyayers/django-tables2/pull/370) by [@graup](https://github.com/graup)). Current behavior should be intact, we will change the default in the future so it will **not** add the column name to the list of CSS classes. ## 1.2.5 (2016-07-30) - Fixed an issue preventing the rest of the row being rendered if a `BooleanColumn` was in the table for a model without custom choices defined on the model field. ([#360](https://github.com/bradleyayers/django-tables2/issues/360)) ## 1.2.4 (2016-07-28) - Added Norwegian Locale ([#356](https://github.com/bradleyayers/django-tables2/issues/356) by [@fanzypantz](https://github.com/fanzypantz)) - Restore default pagination for `SingleTableMixin`, fixes [#354](https://github.com/bradleyayers/django-tables2/issues/354) ([#395](https://github.com/bradleyayers/django-tables2/pull/359) by [@graup](https://github.com/graup)) ## 1.2.3 (2016-07-05) - Accept `text` parameter in `FileColumn`, analogous to `LinkColumn` ([#343](https://github.com/bradleyayers/django-tables2/pull/343) by [@graup](https://github.com/graup)) - Fix TemplateColumn RemovedInDjango110Warning fixes [#346](https://github.com/bradleyayers/django-tables2/issues/346). - Use field name in RelatedColumnLink ([#350](https://github.com/bradleyayers/django-tables2/pull/350), fixes [#347](https://github.com/bradleyayers/django-tables2/issues/347)) ## v1.2.2 (2016-06-04) - Allow use of custom class names for ordered columns through `attrs`. ( [#329](https://github.com/bradleyayers/django-tables2/pull/329) by [@theTarkus](https://github.com/theTarkus)) - Column ordering QuerySet pass through ([#330](https://github.com/bradleyayers/django-tables2/pull/330) by [@theTarkus](https://github.com/theTarkus)) - Cleanup/restructuring of [documentation](http://django-tables2.readthedocs.io/), ([#325](https://github.com/bradleyayers/django-tables2/pull/325)) - Fixed an issue where explicitly defined column options where not preserved over inheritance ([#339](https://github.com/bradleyayers/django-tables2/pull/339), [issue #337](https://github.com/bradleyayers/django-tables2/issues/337)) - Fixed an issue where `exclude` in combination with `sequence` raised a KeyError ([#341](https://github.com/bradleyayers/django-tables2/pull/341), [issue #205](https://github.com/bradleyayers/django-tables2/issues/205)) ## v1.2.1 (2016-05-09) - table footers (#323) - Non-field based `LinkColumn` only renders default value if lookup fails. (#322) - Accept `text` parameter in `BaseLinkColumn`-based columns. (#322) - Pass the table instance into SingleTableMixin's `get_table_pagination` (#320 by [@georgema1982](https://github.com/georgema1982), fixes #319) - Check if the view has `paginate_by` before before trying to access it. (fixes #326) ## v1.2.0 (2016-05-02) - Allow custom attributes for rows (fixes #47) ## v1.1.8 (2016-05-02) - Ability to change the body of the ``-tag, by passing `text` kwarg to the columns inheriting from BaseLinkColumn (#318 by [@desecho](https://github.com/desecho), #322) - Non-field based LinkColumn only renders default value if lookup fails and text is not set. (#322, fixes #257) ## v1.1.7 (2016-04-26) - Added Italian translation (#315 by [@paolodina](https://github.com/paolodina) - Added Dutch translation. - Fixed {% blocktrans %} template whitespace issues - Fixed errors when using a column named `items` (#316) - Obey `paginate_by` (from `MultipleObjectMixin`) if no later pagination is defined (#242) ## v1.1.6 (2016-04-02) - Correct error message about request context processors for current Django (#314) - Skipped 1.1.5 due to an error while creating the tag. ## v1.1.4 (2016-03-22) - Fix broken `setup.py` if Django is not installed before django-tables2 (fixes #312) ## v1.1.3 (2016-03-21) - Drop support for Django 1.7 - Add argument to `CheckBoxColumn` to render it as checked (original PR: #208) ## v1.1.2 (2016-02-16) - Fix `BooleanColumn` with choices set will always render as if `True` (#301) - Fix a bug with `TemplateColumn` while using cached template loader (#75) ## v1.1.1 (2016-01-26) - Allow `Meta.fields` to be a list as well as a tuple (#250) - Call template.render with a dict in Django >= 1.8. (#298) - Added `RelatedLinkColumn()` to render links to related objects (#297) - Remove default value from request parameter to `table.as_html()` ## v1.1.0 (2016-01-19) - Add tests for `TimeColumn` - Remove `sortable` argument for `Table` and Column constructors and its associated methods. Deprecated since 2012. - Remove deprecated aliases for `attrs` in `CheckboxColumn`. - Remove deprecated `OrderByTuple` `cmp` method (deprecated since 2013). - Add bootstrap template and (#293, fixes #141, #285) - Fix different html for tables with and without pagination (#293, fixes #149, #285) - Remove `{% nospaceless %}` template tag and remove wrapping template in `{% spaceless %}` **Possible breaking change**, if you use custom templates. ## v1.0.7 (2016-01-03) - Explicitly check if `column.verbose_name` is not None to support empty column headers (fixes #280) - Cleanup the example project to make it work with modern Django versions. - Do not sort `QuerySet` when `orderable=False` (#204 by [@bmihelac](https://github.com/bmihelac)) - `show_header` attribute on `Table` allows disabling the header (#175 by [@kviktor](https://github.com/kviktor)) - `LinkColumn` now tries to call `get_absolute_url` on a record if no `viewname` is provided (#283, fixes #231). - Add `request` argument to `Table.as_html()` to allow passing correct request objects instead of poorly generated ones #282 - Add coverage reporting to build #282 - Drop support for python 3.2 (because of coverage), support ends February 2016 #282 - move `build_request` from `django_table2.utils` to `tests.utils` and amend tests #282 ## v1.0.6 (2015-12-29) - Support for custom text value in `LinkColumn` (#277 by [@toudi](https://github.com/toudi)) - Refactor `LinkColumn.render_link()` to not escape twice #279 - Removed `Attrs` (wrapper for dict), deprecated on 2012-09-18 - Convert README.md to rst in setup.py to make PyPI look nice (fixes #97) ## v1.0.5 (2015-12-17) - First version released by new maintainer [@jieter](https://github.com/jieter) - Dropped support for Django 1.5 and 1.6, add python 3.5 with Django 1.8 and 1.9 to the build matrix (#273) - Prevent `SingleTableView` from calling `get_queryset` twice. (fixes #155) - Don't call managers when resolving accessors. (#214 by [@mbertheau](https://github.com/mbertheau), fixes #211) ## v1.0.4 (2015-05-09) - Fix bug in retrieving `field.verbose_name` under Django 1.8. ## v1.0.3 - Remove `setup.cfg` as PyPI does not actually support it, instead it is a distutils2 thing that is been discontinued. ## v1.0.2 - Add `setup.cfg` to declare `README.md` for PyPI. ## v1.0.1 - Convert README to markdown so it's formatted nicely on PyPI. ## v1.0.0 - Travis CI builds pass. - Added Python 3.4 support. - Added Django 1.7 and Django 1.8 support. - Convert tests to using `py.test`. ## v0.16.0 - Django 1.8 fixes - `BoundColumn.verbose_name` now only is capitalized only if no verbose_name was given. `verbose_name` is used verbatim. - Add max_length attribute to person CharField - Add Swedish translation - Update docs presentation on readthedocs ## v0.15.0 - Add UK, Russian, Spanish, Portuguese, and Polish translations - Add support for computed table `attrs`. ## v0.14.0 - `querystring` and `seturlparam` template tags now require the request to be in the context (backwards incompatible) -- #127 - Add Travis CI support - Add support for Django 1.5 - Add L10N control for columns #120 (ignored in < Django 1.3) - Drop Python 2.6.4 support in favor of Python 3.2 support - Non-QuerySet data ordering is different between Python 3 and 2. When comparing different types, their truth values are now compared before falling back to string representations of their type. ## v0.13.0 - Add FileColumn. ## v0.12.1 - When resolving an accessor, *all* exceptions are smothered into `None`. ## v0.12.0 - Improve performance by removing unnecessary queries - Simplified pagination: - `Table.page` is an instance attribute (no longer `@property`) - Exceptions raised by paginators (e.g. `EmptyPage`) are no longer smothered by `Table.page` - Pagination exceptions are raised by `Table.paginate` - `RequestConfig` can handles pagination errors silently, can be disabled by including `silent=False` in the `paginate` argument value - Add `DateTimeColumn` and `DateColumn` to handle formatting `datetime` and timezones. - Add `BooleanColumn` to handle bool values - `render_table` can now build and render a table for a QuerySet, rather than needing to be passed a table instance - Table columns created automatically from a model now use specialized columns - `Column.render` is now skipped if the value is considered *empty*, the default value is used instead. Empty values are specified via `Column.empty_values`, by default is `(None, '')` (backward incompatible) - Default values can now be specified on table instances or `Table.Meta` - Accessor's now honor `alters_data` during resolving. Fixes issue that would delete all your data when a column had an accessor of `delete` - Add `default` and `value` to context of `TemplateColumn` - Add cardinality indication to the pagination area of a table - `Attrs` is deprecated, use `dict` instead ## v0.11.0 - Add `URLColumn` to render URLs in a data source into hyperlinks - Add `EmailColumn` to render email addresses into hyperlinks - `TemplateColumn` can now Django's template loaders to render from a file ## v0.10.4 - Fix more bugs on Python 2.6.4, all tests now pass. ## v0.10.3 - Fix issues for Python 2.6.4 -- thanks Steve Sapovits & brianmay - Reduce Django 1.3 dependency to Table.as_html -- thanks brianmay ## v0.10.2 - Fix MANIFEST.in to include example templates, thanks TWAC. - Upgrade django-attest to fix problem with tests on Django 1.3.1 ## v0.10.1 - Fixed support for Django 1.4's paginator (thanks @koledennix) - Some juggling of internal implementation. `TableData` now supports slicing and returns new `TableData` instances. `BoundRows` now takes a single argument `data` (a `TableData` instance). - Add support for `get_pagination` on `SingleTableMixin`. - `SingleTableMixin` and `SingleTableView` are now importable directly from `django_tables2`. ## v0.10.0 - Renamed `BoundColumn.order_by` to `order_by_alias` and never returns `None` (**Backwards incompatible**). Templates are affected if they use something like: {% querystring table.prefixed_order_by_field=column.order_by.opposite|default:column.name %} Which should be rewritten as: {% querystring table.prefixed_order_by_field=column.order_by_alias.next %} - Added `next` shortcut to `OrderBy` returned from `BoundColumn.order_by_alias` - Added `OrderByTuple.get()` - Deprecated `BoundColumn.sortable`, `Column.sortable`, `Table.sortable`, `sortable` CSS class, `BoundColumns.itersortable`, `BoundColumns.sortable`; use `orderable` instead of `sortable`. - Added `BoundColumn.is_ordered` - Introduced concept of an `order by alias`, see glossary in the docs for details. ## v0.9.6 - Fix bug that caused an ordered column's `` to have no HTML attributes. ## v0.9.5 - Updated example project to add `colspan` on footer cell so table border renders correctly in Webkit. - Fix regression that caused 'sortable' class on . - `Table.__init__` no longer *always* calls `.order_by()` on QuerySets, fixes #55. This does introduce a slight backwards incompatibility. `Table.order_by` now has the possibility of returning `None`, previously it would *always* return an `OrderByTuple`. - `DeclarativeColumnsMetaclass.__new__` now uses `super()`` - Testing now requires pylint and Attest >=0.5.3 ## v0.9.4 - Fix regression that caused column verbose_name values that were marked as safe to be escaped. Now any verbose_name values that are instances of SafeData are used unmodified. ## v0.9.3 - Fix regression in `SingleTableMixin`. - Remove stray `print` statement. ## v0.9.2 - `SingleTableView` now uses `RequestConfig`. This fixes issues with `order_by_field`, `page_field`, and `per_page_field` not being honored. - Add `Table.Meta.per_page` and change `Table.paginate` to use it as default. - Add `title` template filter. It differs from Django's built-in `title` filter because it operates on an individual word basis and leaves words containing capitals untouched. **Warning**: use `{% load ... from ... %}` to avoid inadvertently replacing Django's built-in `title` template filter. - `BoundColumn.verbose_name` no longer does `capfirst`, capitalizing is now the responsibility of `Column.header`. - `BoundColumn.__unicode__` now uses `BoundColumn.header` rather than `BoundColumn.verbose_name`. ## v0.9.1 - Fix version in `setup.py` ## v0.9.0 - Add support for column attributes (see Attrs) - Add `BoundRows.items()` to yield `(bound_column, cell)` pairs - Tried to make docs more concise. Much stronger promotion of using `RequestConfig` and `{% querystring %}` ## v0.8.4 - Removed random 'print' statements. - Tweaked `paleblue` theme css to be more flexible: - removed `whitespace: no-wrap` - header background image to support more than 2 rows of text ## v0.8.3 - Fixed stupid import mistake. Tests did not pick it up due to them ignoring `ImportError`. ## v0.8.2 - `SingleTableView` now inherits from `ListView` which enables automatic `foo_list.html` template name resolution (thanks dramon for reporting) - `render_table` template tag no suppresses exceptions when `DEBUG=True` ## v0.8.1 - Fixed bug in render_table when giving it a template (issue #41) ## v0.8.0 - Added translation support in the default template via `{% trans %}` - Removed `basic_table.html`, `Table.as_html()` now renders `table.html` but will clobber the query string of the current request. Use the `render_table` template tag instead - `render_table` now supports an optional second argument -- the template to use when rendering the table - `Table` now supports declaring which template to use when rendering to HTML - Django >=1.3 is now required - Added support for using django-haystack's `SearchQuerySet` as a data source - The default template `table.html` now includes block tags to make it easy to extend to change small pieces - Fixed table template parsing problems being hidden due to a subsequent exception being raised - `Http404` exceptions are no longer raised during a call to `Table.paginate()`, instead it now occurs when `Table.page` is accessed - Fixed bug where a table could not be rendered more than once if it was paginated. - Accessing `Table.page` now returns a new page every time, rather than reusing a single object ## v0.7.8 - Tables now support using both `sequence` and `exclude` (issue #32). - `Sequence` class moved to `django_tables2/utils.py`. - Table instances now support modification to the `exclude` property. - Removed `BoundColumns._spawn_columns`. - `Table.data`, `Table.rows`, and `Table.columns` are now attributes rather than properties. django-tables2-2.1.1/example/0000755000175000017500000000000013542204315015303 5ustar josephjosephdjango-tables2-2.1.1/example/urls.py0000644000175000017500000000266713542204315016655 0ustar josephjosephfrom django.conf import settings from django.contrib import admin from django.urls import include, path from django.views import static from app.views import ( ClassBased, FilteredPersonListView, MultipleTables, bootstrap, bootstrap4, checkbox, country_detail, index, multiple, person_detail, semantic, tutorial, ) urlpatterns = [ path("", index), path("multiple/", multiple, name="multiple"), path("class-based/", ClassBased.as_view(), name="singletableview"), path("class-based-multiple/", MultipleTables.as_view(), name="multitableview"), path("class-based-filtered/", FilteredPersonListView.as_view(), name="filtertableview"), path("checkbox/", checkbox, name="checkbox"), path("tutorial/", tutorial, name="tutorial"), path("bootstrap/", bootstrap, name="bootstrap"), path("bootstrap4/", bootstrap4, name="bootstrap4"), path("semantic/", semantic, name="semantic"), path("admin/doc/", include("django.contrib.admindocs.urls")), path("admin/", admin.site.urls), path("country//", country_detail, name="country_detail"), path("person//", person_detail, name="person_detail"), path("media/", static.serve, {"document_root": settings.MEDIA_ROOT}), path("i18n/", include("django.conf.urls.i18n")), ] if settings.DEBUG: import debug_toolbar urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns django-tables2-2.1.1/example/settings.py0000644000175000017500000001203213542204315017513 0ustar josephjosephfrom os.path import abspath, dirname, join from django.utils.translation import gettext_lazy as _ ROOT = dirname(abspath(__file__)) DEBUG = True ADMINS = ( # ('Your Name', 'your_email@example.com'), ) ALLOWED_HOSTS = ["*"] MANAGERS = ADMINS DATABASES = { "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": join(ROOT, "database.sqlite")} } # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. # On Unix systems, a value of None will cause Django to use the same # timezone as the operating system. # If running in a Windows environment this must be set to the same as your # system time zone. TIME_ZONE = "America/Chicago" # Language code for this installation. All choices can be found here: # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = "en" SITE_ID = 1 # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. USE_I18N = True LANGUAGES = [ ("cs", _("Czech")), ("de", _("German")), ("el", _("Greek")), ("en", _("English")), ("es", _("Spanish")), ("fr", _("French")), ("hu", _("Hungarian")), ("it", _("Italian")), ("nb", _("Norwegian bokmål")), ("nl", _("Dutch")), ("pl", _("Polish")), ("pt-br", _("Portuguese (Brasil)")), ("pt-pt", _("Portuguese (Portugal)")), ("ru", _("Russian")), ("sv", _("Swedish")), ("uk", _("Ukrainian")), ("zh-hans", _("Chinese (Simplified)")), ] # If you set this to False, Django will not format dates, numbers and # calendars according to the current locale USE_L10N = True # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: "/home/media/media.lawrence.com/media/" MEDIA_ROOT = join(ROOT, "media") # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash. # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" MEDIA_URL = "/media/" # Absolute path to the directory static files should be collected to. # Don't put anything in this directory yourself; store your static files # in apps' "static/" subdirectories and in STATICFILES_DIRS. # Example: "/home/media/media.lawrence.com/static/" STATIC_ROOT = "" # URL prefix for static files. # Example: "http://media.lawrence.com/static/" STATIC_URL = "/static/" # URL prefix for admin static files -- CSS, JavaScript and images. # Make sure to use a trailing slash. # Examples: "http://foo.com/static/admin/", "/static/admin/". ADMIN_MEDIA_PREFIX = "/static/admin/" # Additional locations of static files STATICFILES_DIRS = ( # Put strings here, like "/home/html/static" or "C:/www/django/static". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. ) # List of finder classes that know how to find static files in # various locations. STATICFILES_FINDERS = ( "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", # 'django.contrib.staticfiles.finders.DefaultStorageFinder', ) # Make this unique, and don't share it with anybody. SECRET_KEY = "=nzw@mkqk)tz+_#vf%li&8sn7yn8z7!2-4njuyf1rxs*^muhvh" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": ["templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.contrib.auth.context_processors.auth", "django.template.context_processors.request", "django.template.context_processors.static", ] }, } ] MIDDLEWARE = ( "debug_toolbar.middleware.DebugToolbarMiddleware", "django.middleware.common.CommonMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.locale.LocaleMiddleware", ) ROOT_URLCONF = "urls" INSTALLED_APPS = ( "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.sites", "django.contrib.messages", "django.contrib.staticfiles", "django_filters", "bootstrap3", "bootstrap4", "django_tables2", "debug_toolbar", "app", ) INTERNAL_IPS = ("127.0.0.1",) # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to # the site admins on every HTTP 500 error. # See http://docs.djangoproject.com/en/dev/topics/logging for # more details on how to customize your logging configuration. LOGGING = { "version": 1, "disable_existing_loggers": False, "handlers": {"mail_admins": {"level": "ERROR", "class": "django.utils.log.AdminEmailHandler"}}, "loggers": { "django.request": {"handlers": ["mail_admins"], "level": "ERROR", "propagate": True} }, } django-tables2-2.1.1/example/app/0000755000175000017500000000000013542204315016063 5ustar josephjosephdjango-tables2-2.1.1/example/app/views.py0000644000175000017500000001401413542204315017572 0ustar josephjosephfrom random import choice from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.utils.lorem_ipsum import words from django.views.generic.base import TemplateView from django_filters.views import FilterView from django_tables2 import MultiTableMixin, RequestConfig, SingleTableMixin, SingleTableView from django_tables2.export.views import ExportMixin from django_tables2.paginators import LazyPaginator from .data import COUNTRIES from .filters import PersonFilter from .models import Country, Person from .tables import ( Bootstrap4Table, BootstrapTable, BootstrapTablePinnedRows, CheckboxTable, CountryTable, PersonTable, SemanticTable, ThemedCountryTable, ) def create_fake_data(): # create some fake data to make sure we need to paginate if Country.objects.all().count() < 50: for country in COUNTRIES.splitlines(): name, population = country.split(";") Country.objects.create(name=name, visits=0, population=int(population)) if Person.objects.all().count() < 500: countries = list(Country.objects.all()) + [None] Person.objects.bulk_create( [Person(name=words(3, common=False), country=choice(countries)) for i in range(50)] ) def index(request): create_fake_data() table = PersonTable(Person.objects.all()) RequestConfig(request, paginate={"per_page": 5}).configure(table) return render( request, "index.html", { "table": table, "urls": ( (reverse("tutorial"), "Tutorial"), (reverse("multiple"), "Multiple tables"), (reverse("filtertableview"), "Filtered tables (with export)"), (reverse("singletableview"), "Using SingleTableMixin"), (reverse("multitableview"), "Using MultiTableMixin"), (reverse("bootstrap"), "template: Bootstrap 3 (bootstrap.html)"), (reverse("bootstrap4"), "template: Bootstrap 4 (bootstrap4.html)"), (reverse("semantic"), "template: Semantic UI (semantic.html)"), ), }, ) def multiple(request): qs = Country.objects.all() example1 = CountryTable(qs, prefix="1-") RequestConfig(request, paginate=False).configure(example1) example2 = CountryTable(qs, prefix="2-") RequestConfig(request, paginate={"per_page": 2}).configure(example2) example3 = ThemedCountryTable(qs, prefix="3-") RequestConfig(request, paginate={"per_page": 3}).configure(example3) example4 = ThemedCountryTable(qs, prefix="4-") RequestConfig(request, paginate={"per_page": 3}).configure(example4) example5 = ThemedCountryTable(qs, prefix="5-") example5.template = "extended_table.html" RequestConfig(request, paginate={"per_page": 3}).configure(example5) return render( request, "multiple.html", { "example1": example1, "example2": example2, "example3": example3, "example4": example4, "example5": example5, }, ) def checkbox(request): create_fake_data() table = CheckboxTable(Country.objects.all(), order_by="name") RequestConfig(request, paginate={"per_page": 15}).configure(table) return render(request, "checkbox_example.html", {"table": table}) def bootstrap(request): """Demonstrate the use of the bootstrap template""" create_fake_data() table = BootstrapTable(Person.objects.all().select_related("country"), order_by="-name") RequestConfig(request, paginate={"paginator_class": LazyPaginator, "per_page": 10}).configure( table ) return render(request, "bootstrap_template.html", {"table": table}) def bootstrap4(request): """Demonstrate the use of the bootstrap4 template""" create_fake_data() table = Bootstrap4Table(Person.objects.all(), order_by="-name") RequestConfig(request, paginate={"per_page": 10}).configure(table) return render(request, "bootstrap4_template.html", {"table": table}) def semantic(request): """Demonstrate the use of the Semantic UI template""" create_fake_data() table = SemanticTable(Person.objects.all(), order_by="-name") RequestConfig(request, paginate={"per_page": 10}).configure(table) return render(request, "semantic_template.html", {"table": table}) class ClassBased(SingleTableView): table_class = ThemedCountryTable queryset = Country.objects.all() template_name = "class_based.html" class MultipleTables(MultiTableMixin, TemplateView): template_name = "multiTable.html" table_pagination = {"per_page": 10} def get_tables(self): qs = Person.objects.all() return [ PersonTable(qs), PersonTable(qs, exclude=("country",)), BootstrapTablePinnedRows(qs), ] def tutorial(request): table = PersonTable( Person.objects.all(), attrs={"class": "paleblue"}, template_name="django_tables2/table.html" ) RequestConfig(request, paginate={"per_page": 10}).configure(table) return render(request, "tutorial.html", {"table": table}) class FilteredPersonListView(ExportMixin, SingleTableMixin, FilterView): table_class = PersonTable model = Person template_name = "bootstrap_template.html" filterset_class = PersonFilter export_formats = ("csv", "xls") def get_queryset(self): return super().get_queryset().select_related("country") def get_table_kwargs(self): return {"template_name": "django_tables2/bootstrap.html"} def country_detail(request, pk): country = get_object_or_404(Country, pk=pk) # hide the country column, as it is not very interesting for a list of persons for a country. table = PersonTable(country.person_set.all(), extra_columns=(("country", None),)) return render(request, "country_detail.html", {"country": country, "table": table}) def person_detail(request, pk): person = get_object_or_404(Person, pk=pk) return render(request, "person_detail.html", {"person": person}) django-tables2-2.1.1/example/app/migrations/0000755000175000017500000000000013542204315020237 5ustar josephjosephdjango-tables2-2.1.1/example/app/migrations/0003_auto_20180416_1020.py0000644000175000017500000000102013542204315023643 0ustar josephjoseph# Generated by Django 2.0.1 on 2018-04-16 10:20 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [("app", "0002_auto_20180416_0959")] operations = [ migrations.AlterField( model_name="country", name="flag", field=models.FileField(blank=True, upload_to="country/flags/"), ), migrations.AlterField( model_name="country", name="tz", field=models.CharField(blank=True, max_length=50) ), ] django-tables2-2.1.1/example/app/migrations/0002_auto_20180416_0959.py0000644000175000017500000000155013542204315023676 0ustar josephjoseph# Generated by Django 2.0.1 on 2018-04-16 09:59 from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [("app", "0001_initial")] operations = [ migrations.CreateModel( name="Continent", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), ("name", models.CharField(max_length=100)), ], ), migrations.AddField( model_name="country", name="continent", field=models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, to="app.Continent" ), ), ] django-tables2-2.1.1/example/app/migrations/0001_initial.py0000644000175000017500000000342713542204315022710 0ustar josephjoseph# Generated by Django 1.11.5 on 2017-09-22 13:23 from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name="Country", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), ("name", models.CharField(max_length=100)), ("population", models.PositiveIntegerField(verbose_name="population")), ("tz", models.CharField(max_length=50)), ("visits", models.PositiveIntegerField()), ("commonwealth", models.NullBooleanField()), ("flag", models.FileField(upload_to="country/flags/")), ], options={"verbose_name_plural": "countries"}, ), migrations.CreateModel( name="Person", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), ("name", models.CharField(max_length=200, verbose_name="full name")), ("friendly", models.BooleanField(default=True)), ( "country", models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, to="app.Country" ), ), ], options={"verbose_name_plural": "people"}, ), ] django-tables2-2.1.1/example/app/migrations/__init__.py0000644000175000017500000000000013542204315022336 0ustar josephjosephdjango-tables2-2.1.1/example/app/tables.py0000644000175000017500000000425213542204315017712 0ustar josephjosephimport django_tables2 as tables from .models import Country, Person class CountryTable(tables.Table): name = tables.Column() population = tables.Column() tz = tables.Column(verbose_name="time zone") visits = tables.Column() summary = tables.Column(order_by=("name", "population")) class Meta: model = Country class ThemedCountryTable(CountryTable): class Meta: attrs = {"class": "paleblue"} class CheckboxTable(tables.Table): select = tables.CheckBoxColumn(empty_values=(), footer="") population = tables.Column(attrs={"cell": {"class": "population"}}) class Meta: model = Country template_name = "django_tables2/bootstrap.html" fields = ("select", "name", "population") class BootstrapTable(tables.Table): id = tables.Column(linkify=True) country = tables.Column(linkify=True) continent = tables.Column( accessor="country.continent.name", verbose_name="Continent", linkify=True ) class Meta: model = Person template_name = "django_tables2/bootstrap.html" exclude = ("friendly",) class BootstrapTablePinnedRows(BootstrapTable): class Meta(BootstrapTable.Meta): pinned_row_attrs = {"class": "info"} def get_top_pinned_data(self): return [ { "name": "Most used country: ", "country": Country.objects.filter(name="Cameroon").first(), } ] class Bootstrap4Table(tables.Table): country = tables.Column(linkify=True) continent = tables.Column(accessor="country.continent", linkify=True) class Meta: model = Person template_name = "django_tables2/bootstrap4.html" attrs = {"class": "table table-hover"} exclude = ("friendly",) class SemanticTable(tables.Table): country = tables.RelatedLinkColumn() class Meta: model = Person template_name = "django_tables2/semantic.html" exclude = ("friendly",) class PersonTable(tables.Table): id = tables.Column(linkify=True) country = tables.Column(linkify=True) class Meta: model = Person template_name = "django_tables2/bootstrap.html" django-tables2-2.1.1/example/app/__init__.py0000644000175000017500000000000013542204315020162 0ustar josephjosephdjango-tables2-2.1.1/example/app/data.py0000644000175000017500000000155213542204315017351 0ustar josephjosephCOUNTRIES = """Aruba;104822 Afghanistan;34656032 Angola;28813463 Albania;2876101 Andorra;77281 Arab World;406452690 United Arab Emirates;9269612 Argentina;43847430 Armenia;2924816 American Samoa;55599 Antigua and Barbuda;100963 Australia;24127159 Austria;8747358 Azerbaijan;9762274 Burundi;10524117 Belgium;11348159 Benin;10872298 Burkina Faso;18646433 Bangladesh;162951560 Bulgaria;7127822 Bahrain;1425171 Bahamas, The;391232 Bosnia and Herzegovina;3516816 Belarus;9507120 Belize;366954 Bermuda;65331 Bolivia;10887882 Brazil;207652865 Barbados;284996 Brunei Darussalam;423196 Bhutan;797765 Botswana;2250260 Central African Republic;4594621 Canada;36286425 Switzerland;8372098 Channel Islands;164541 Chile;17909754 China;1378665000 Cote d'Ivoire;23695919 Cameroon;23439189 Congo, Dem. Rep.;78736153 Congo, Rep.;5125821 Colombia;48653419 Comoros;795601 Cabo Verde;539560 """ django-tables2-2.1.1/example/app/fixtures/0000755000175000017500000000000013542204315017734 5ustar josephjosephdjango-tables2-2.1.1/example/app/fixtures/initial_data.json0000644000175000017500000000145113542204315023252 0ustar josephjoseph[ { "pk": 1, "model": "app.country", "fields": { "tz": "Australia/Brisbane", "name": "Australia", "visits": 2, "population": 20000000, "flag": "country/flags/australia.svg" } }, { "pk": 2, "model": "app.country", "fields": { "tz": "NZST", "name": "New Zealand", "visits": 1, "population": 12000000, "flag": "country/flags/new_zealand.svg" } }, { "pk": 4, "model": "app.country", "fields": { "tz": "UTC\u22123.5", "name": "Canada", "visits": 1, "population": 34447000, "flag": "country/flags/canada.svg" } } ] django-tables2-2.1.1/example/app/admin.py0000644000175000017500000000037613542204315017533 0ustar josephjosephfrom django.contrib import admin from .models import Continent, Country class CountryAdmin(admin.ModelAdmin): list_per_page = 20 list_display = ("name", "continent") admin.site.register(Country, CountryAdmin) admin.site.register(Continent) django-tables2-2.1.1/example/app/models.py0000644000175000017500000000266713542204315017733 0ustar josephjosephfrom django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ class Continent(models.Model): name = models.CharField(max_length=100) def __str__(self): return self.name class Country(models.Model): """ Represents a geographical Country """ name = models.CharField(max_length=100) population = models.PositiveIntegerField(verbose_name=_("population")) tz = models.CharField(max_length=50, blank=True) visits = models.PositiveIntegerField() commonwealth = models.NullBooleanField() flag = models.FileField(upload_to="country/flags/", blank=True) continent = models.ForeignKey(Continent, null=True, on_delete=models.CASCADE) class Meta: verbose_name_plural = _("countries") def __str__(self): return self.name def get_absolute_url(self): return reverse("country_detail", args=(self.pk,)) @property def summary(self): return "%s (pop. %s)" % (self.name, self.population) class Person(models.Model): name = models.CharField(max_length=200, verbose_name="full name") friendly = models.BooleanField(default=True) country = models.ForeignKey(Country, null=True, on_delete=models.CASCADE) class Meta: verbose_name_plural = "people" def __str__(self): return self.name def get_absolute_url(self): return reverse("person_detail", args=(self.pk,)) django-tables2-2.1.1/example/app/filters.py0000644000175000017500000000032013542204315020100 0ustar josephjosephfrom django_filters import FilterSet from .models import Person class PersonFilter(FilterSet): class Meta: model = Person fields = {"name": ["exact", "contains"], "country": ["exact"]} django-tables2-2.1.1/example/__init__.py0000644000175000017500000000000013542204315017402 0ustar josephjosephdjango-tables2-2.1.1/example/manage.py0000755000175000017500000000036313542204315017112 0ustar josephjoseph#!/usr/bin/env python3 import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) django-tables2-2.1.1/example/templates/0000755000175000017500000000000013542204315017301 5ustar josephjosephdjango-tables2-2.1.1/example/templates/bootstrap4_template.html0000644000175000017500000000216113542204315024163 0ustar josephjoseph{% load static %} {% load render_table from django_tables2 %} {% load bootstrap4 %} django_tables2 with bootstrap 4 template example {% bootstrap_css %}
{% block body %} Bootstrap 4 - tables docs | Bootstrap 4 - pagination docs

django_tables2 with Bootstrap 4 template example

{% if filter %}
{% bootstrap_form filter.form layout='inline' %} {% bootstrap_button 'filter' %}
{% endif %}
{% render_table table %}
{% endblock %}
django-tables2-2.1.1/example/templates/country_detail.html0000644000175000017500000000032313542204315023212 0ustar josephjoseph{% extends 'semantic_template.html' %} {% load django_tables2 %} {% block body %}

{{ country }}

Persons in this country

{% render_table table 'django_tables2/semantic.html' %} {% endblock %} django-tables2-2.1.1/example/templates/extended_table.html0000644000175000017500000000027513542204315023142 0ustar josephjoseph{% extends "django_tables2/table.html" %} {% block table.tfoot %} This is a footer {% endblock %} django-tables2-2.1.1/example/templates/multiTable.html0000644000175000017500000000137613542204315022300 0ustar josephjoseph{% extends "base.html" %} {% load django_tables2 %} {% load static %} {% block extrahead %} {% endblock %} {% block body %}

Multiple tables on a single page using MultiTableMixin

Pagination should work independently for each table, the second table excludes the column 'country'.

{% for table in tables %}
{% render_table table %}
{% endfor %}
{% endblock %} django-tables2-2.1.1/example/templates/multiple.html0000644000175000017500000000461313542204315022026 0ustar josephjoseph{% extends "base.html" %} {% block body %}

django-tables2 examples

This page demonstrates various types of tables being rendered via django-tables2.

Example 1 — QuerySet

via as_html()

{% templatetag openvariable %} example1.as_html {% templatetag closevariable %}
{{ example1.as_html }}

via template tag

{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
{% templatetag openblock %} render_table example1 {% templatetag closeblock %}
{% load django_tables2 %} {% render_table example1 %}

Example 2 — QuerySet + pagination

via as_html()

{% templatetag openvariable %} example2.as_html {% templatetag closevariable %}
{{ example2.as_html }}

via template tag

{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
{% templatetag openblock %} render_table example2 {% templatetag closeblock %}
{% load django_tables2 %} {% render_table example2 %}

Example 3 — QuerySet + paleblue theme

via as_html()

{% templatetag openvariable %} example3.as_html {% templatetag closevariable %}
{{ example3.as_html }}

via template tag

{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
{% templatetag openblock %} render_table example3 {% templatetag closeblock %}
{% load django_tables2 %} {% render_table example3 %}

Example 4 — QuerySet + pagination + paleblue theme

via as_html()

{% templatetag openvariable %} example4.as_html {% templatetag closevariable %}
{{ example4.as_html }}

via template tag

{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
{% templatetag openblock %} render_table example4 {% templatetag closeblock %}
{% load django_tables2 %} {% render_table example4 %}

Example 5 – QuerySet + pagination + paleblue theme + custom template

via as_html()

{% templatetag openvariable %} example5.as_html {% templatetag closevariable %}
{{ example5.as_html }}

via template tag

{% templatetag openblock %} load django_tables2 {% templatetag closeblock %}
{% templatetag openblock %} render_table example5 {% templatetag closeblock %}
{% load django_tables2 %} {% render_table example5 %} {% endblock %} django-tables2-2.1.1/example/templates/checkbox_example.html0000644000175000017500000000145313542204315023473 0ustar josephjoseph{% extends 'bootstrap_template.html' %} {% load bootstrap3 %} {% block title %}django-tables2 CheckBoxColumn example{% endblock %} {% block body %} {{ block.super }} {% bootstrap_javascript jquery=True %} {% endblock %} django-tables2-2.1.1/example/templates/index.html0000644000175000017500000000560313542204315021302 0ustar josephjoseph{% extends "base.html" %} {% load django_tables2 %} {% load static %} {% load i18n %} {% block extrahead %} {% endblock %} {% block body %}

Some examples of using django-tables2

Welcome to the django-tables2 example project. Below is a list of different examples using django-tables2.

Basic example of a table

{% render_table table "django_tables2/bootstrap.html" %}

Same table, but responsive (.table-responsive)

{% render_table table "django_tables2/bootstrap-responsive.html" %}
{% endblock %} django-tables2-2.1.1/example/templates/semantic_template.html0000644000175000017500000000127613542204315023673 0ustar josephjoseph{% load render_table from django_tables2 %} django_tables2 with semantic template example
{% block body %}

django_tables2 with Semantic UI template example

{% render_table table %} {% endblock %}
django-tables2-2.1.1/example/templates/tutorial.html0000644000175000017500000000050313542204315022030 0ustar josephjoseph{% load django_tables2 %} {% load static %}

django_tables2 with the default template example

{% render_table table %} django-tables2-2.1.1/example/templates/bootstrap_template.html0000644000175000017500000000317613542204315024106 0ustar josephjoseph{% load static %} {% load django_tables2 %} {% load bootstrap3 %} django_tables2 with bootstrap 3 template example {% bootstrap_css %}
{% block body %} Bootstrap 3 - table docs | Bootstrap 3 - pagination docs

{% block title %}django_tables2 with Bootstrap 3 template example{% endblock %}

{% if view.export_formats %} {% for format in view.export_formats %} download .{{ format }} {% endfor %} {% endif %}
{% if filter %}
{% bootstrap_form filter.form layout='inline' %} {% bootstrap_button 'filter' %}
{% endif %}
{% render_table table %}
{% endblock %}
django-tables2-2.1.1/example/templates/base.html0000644000175000017500000000115113542204315021077 0ustar josephjoseph{% load static %} django-tables2 examples {% block extrahead %}{% endblock %} {% block body %} {% load django_tables2 %} {% render_table table %} {% endblock %} django-tables2-2.1.1/example/templates/person_detail.html0000644000175000017500000000031313542204315023014 0ustar josephjoseph{% extends 'semantic_template.html' %} {% load django_tables2 %} {% block body %}

{{ person }}

country: {{ person.country }} {% endblock %} django-tables2-2.1.1/example/templates/class_based.html0000644000175000017500000000037513542204315022437 0ustar josephjoseph{% extends "base.html" %} {% load django_tables2 %} {% block body %}

class based view via render_table

{{ "{%" }} load django_tables2 {{ "%}" }}
{{ "{%" }} render_table example {{ "%}" }}
{% render_table table %} {% endblock %} django-tables2-2.1.1/example/media/0000755000175000017500000000000013542204315016362 5ustar josephjosephdjango-tables2-2.1.1/example/media/country/0000755000175000017500000000000013542204315020065 5ustar josephjosephdjango-tables2-2.1.1/example/media/country/flags/0000755000175000017500000000000013542204315021161 5ustar josephjosephdjango-tables2-2.1.1/example/media/country/flags/new_zealand.svg0000644000175000017500000000317413542204315024176 0ustar josephjoseph Flag of New Zealand django-tables2-2.1.1/example/media/country/flags/canada.svg0000644000175000017500000000261713542204315023117 0ustar josephjosephdjango-tables2-2.1.1/example/media/country/flags/australia.svg0000644000175000017500000000450513542204315023673 0ustar josephjoseph django-tables2-2.1.1/example/README.md0000644000175000017500000000064413542204315016566 0ustar josephjoseph# Django-tables2 example project This example project only supports the latest version of Django. # To get it up and running: ``` git clone https://github.com/bradleyayers/django-tables2.git cd django-tables2/example pip install -r requirements.txt python manage.py migrate python manage.py loaddata app/fixtures/initial_data.json python manage.py runserver ``` Server should be live at http://127.0.0.1:8000/ now. django-tables2-2.1.1/example/requirements.txt0000644000175000017500000000016613542204315020572 0ustar josephjoseph-e .. django-bootstrap3==11.0.0 django-bootstrap4==0.0.8 django-debug-toolbar==1.11 django-filter==2.2 tablib==0.13.0 django-tables2-2.1.1/.gitignore0000644000175000017500000000042213542204315015636 0ustar josephjoseph*.pyc /.env /reports /*.sublime-* /*.komodoproject /*.tmproj /*.egg-info/ /*.egg /.tox /.coverage /MANIFEST /dist/ /build/ /docs/_build/ /docs/pages/CHANGELOG.md /example/database.sqlite /example/.env /report.pylint .cache/ .python-version .idea *.sw[po] pip-wheel-metadata django-tables2-2.1.1/.travis.yml0000644000175000017500000000231613542204315015763 0ustar josephjosephdist: xenial language: python cache: pip: true addons: apt: packages: - enchant - myspell-en-us install: - pip install tox coverage script: - tox matrix: include: - { python: 3.5, env: TOXENV=py35-2.1 } - { python: 3.5, env: TOXENV=py35-2.2 } - { python: 3.6, env: TOXENV=py36-2.1 } - { python: 3.6, env: TOXENV=py36-2.2 } - { python: 3.6, env: TOXENV=py36-3.0 } - { python: 3.6, env: TOXENV=py36-master } - { python: 3.7, env: TOXENV=py37-2.1 } - { python: 3.7, env: TOXENV=py37-2.2 } - { python: 3.7, env: TOXENV=py37-3.0 } - { python: 3.7, env: TOXENV=py37-master } - { python: 3.8-dev, env: TOXENV=py38-3.0 } - { python: 3.8-dev, env: TOXENV=py38-master } - { python: 3.6, env: TOXENV=docs } - { python: 3.6, env: TOXENV=flake8 } - { python: 3.6, env: TOXENV=isort } - { python: 3.6, env: TOXENV=black } # we allow failures for versions which are not yet released: allow_failures: - env: TOXENV=py35-master - env: TOXENV=py36-master - env: TOXENV=py37-master - env: TOXENV=py38-master after_success: - pip combine --amend - pip report -m - pip install codecov - codecov django-tables2-2.1.1/tests/0000755000175000017500000000000013542204315015012 5ustar josephjosephdjango-tables2-2.1.1/tests/test_footer.py0000644000175000017500000000721013542204315017721 0ustar josephjosephfrom django.test import SimpleTestCase import django_tables2 as tables from .utils import build_request, parse MEMORY_DATA = [ {"name": "Queensland", "country": "Australia", "population": 4750500}, {"name": "New South Wales", "country": "Australia", "population": 7565500}, {"name": "Victoria", "country": "Australia", "population": 6000000}, {"name": "Tasmania", "country": "Australia", "population": 517000}, ] class FooterTest(SimpleTestCase): def test_has_footer_is_False_without_footer(self): class Table(tables.Table): name = tables.Column() country = tables.Column() population = tables.Column() table = Table(MEMORY_DATA) self.assertFalse(table.has_footer()) def test_footer(self): class Table(tables.Table): name = tables.Column() country = tables.Column(footer="Total:") population = tables.Column( footer=lambda table: sum(x["population"] for x in table.data) ) table = Table(MEMORY_DATA) self.assertTrue(table.has_footer()) html = table.as_html(build_request("/")) columns = parse(html).findall(".//tfoot/tr/td") self.assertEqual(columns[1].text, "Total:") self.assertEqual(columns[2].text, "18833000") def test_footer_disable_on_table(self): """ Showing the footer can be disabled using show_footer argument to the Table constructor """ class Table(tables.Table): name = tables.Column() country = tables.Column(footer="Total:") table = Table(MEMORY_DATA, show_footer=False) self.assertFalse(table.has_footer()) def test_footer_column_method(self): class SummingColumn(tables.Column): def render_footer(self, bound_column, table): return sum(bound_column.accessor.resolve(row) for row in table.data) class TestTable(tables.Table): name = tables.Column() country = tables.Column(footer="Total:") population = SummingColumn() table = TestTable(MEMORY_DATA) html = table.as_html(build_request("/")) columns = parse(html).findall(".//tfoot/tr/td") self.assertEqual(columns[1].text, "Total:") self.assertEqual(columns[2].text, "18833000") def test_footer_has_class(self): class SummingColumn(tables.Column): def render_footer(self, bound_column, table): return sum(bound_column.accessor.resolve(row) for row in table.data) class TestTable(tables.Table): name = tables.Column() country = tables.Column(footer="Total:") population = SummingColumn(attrs={"tf": {"class": "population_sum"}}) table = TestTable(MEMORY_DATA) html = table.as_html(build_request("/")) columns = parse(html).findall(".//tfoot/tr/td") self.assertEqual(columns[2].attrib, {"class": "population_sum"}) def test_footer_custom_attriubtes(self): class SummingColumn(tables.Column): def render_footer(self, bound_column, table): return sum(bound_column.accessor.resolve(row) for row in table.data) class TestTable(tables.Table): name = tables.Column() country = tables.Column(footer="Total:", attrs={"tf": {"align": "right"}}) population = SummingColumn() table = TestTable(MEMORY_DATA) table.columns["country"].attrs["tf"] = {"align": "right"} html = table.as_html(build_request("/")) columns = parse(html).findall(".//tfoot/tr/td") assert "align" in columns[1].attrib django-tables2-2.1.1/tests/test_tabledata.py0000644000175000017500000001062213542204315020345 0ustar josephjosephimport warnings from django.test import TestCase from django_tables2 import Table from django_tables2.data import TableData, TableListData, TableQuerysetData from .app.models import Person, Region from .utils import build_request class TableDataFactoryTest(TestCase): def test_invalid_data_None(self): with self.assertRaises(ValueError): TableData.from_data(None) def test_invalid_data_int(self): with self.assertRaises(ValueError): TableData.from_data(1) def test_invalid_data_classes(self): class Klass: pass with self.assertRaises(ValueError): TableData.from_data(Klass()) class Bad: def __len__(self): pass with self.assertRaises(ValueError): TableData.from_data(Bad()) def test_valid_QuerySet(self): data = TableData.from_data(Person.objects.all()) self.assertIsInstance(data, TableQuerysetData) def test_valid_list_of_dicts(self): data = TableData.from_data([{"name": "John"}, {"name": "Pete"}]) self.assertIsInstance(data, TableListData) self.assertEqual(len(data), 2) def test_valid_tuple_of_dicts(self): data = TableData.from_data(({"name": "John"}, {"name": "Pete"})) self.assertIsInstance(data, TableListData) self.assertEqual(len(data), 2) def test_valid_class(self): class Datasource: def __len__(self): return 1 def __getitem__(self, pos): if pos != 0: raise IndexError() return {"a": 1} data = TableData.from_data(Datasource()) self.assertEqual(len(data), 1) class TableDataTest(TestCase): def test_knows_its_default_name(self): data = TableData.from_data([{}]) self.assertEqual(data.verbose_name, "item") self.assertEqual(data.verbose_name_plural, "items") def test_knows_its_name(self): data = TableData.from_data(Person.objects.all()) self.assertEqual(data.verbose_name, "person") self.assertEqual(data.verbose_name_plural, "people") def generator(max_value): for i in range(max_value): yield {"foo": i, "bar": chr(i), "baz": hex(i), "inv": max_value - i} class TableListsDataTest(TestCase): def test_TableListData_basic_list(self): list_data = list(generator(100)) data = TableListData(list_data) self.assertEqual(len(list_data), len(data)) self.assertEqual(data.verbose_name, "item") self.assertEqual(data.verbose_name_plural, "items") def test_TableListData_with_verbose_name(self): """ TableListData uses the attributes on the listlike object to generate it's verbose_name. """ class listlike(list): verbose_name = "unit" verbose_name_plural = "units" list_data = listlike(generator(100)) data = TableListData(list_data) self.assertEqual(len(list_data), len(data)) self.assertEqual(data.verbose_name, "unit") self.assertEqual(data.verbose_name_plural, "units") class TableQuerysetDataTest(TestCase): def test_custom_TableData(self): """If TableQuerysetData._length is set, no count() query will be performed""" for i in range(20): Person.objects.create(first_name="first {}".format(i)) data = TableQuerysetData(Person.objects.all()) data._length = 10 table = Table(data=data) self.assertEqual(len(table.data), 10) def test_model_mismatch(self): class MyTable(Table): class Meta: model = Person with warnings.catch_warnings(record=True): MyTable(Region.objects.all()) def test_queryset_union(self): for i in range(10): Person.objects.create(first_name="first {}".format(i), last_name="foo") Person.objects.create(first_name="first {}".format(i * 2), last_name="bar") class MyTable(Table): class Meta: model = Person fields = ("first_name",) qs = Person.objects.filter(last_name="bar").union(Person.objects.filter(last_name="foo")) table = MyTable(qs.order_by("-last_name")) self.assertEqual(len(table.rows), 20) html = table.as_html(build_request()) self.assertIn("first 18", html) self.assertIn("first 10", html) django-tables2-2.1.1/tests/test_export.py0000644000175000017500000003106313542204315017747 0ustar josephjosephimport json from datetime import date, datetime, time from unittest import skipIf import pytz from django.core.exceptions import ImproperlyConfigured from django.shortcuts import render from django.test import TestCase import django_tables2 as tables from django_tables2.config import RequestConfig from .app.models import Occupation, Person, Region from .utils import build_request try: from django_tables2.export.export import TableExport from django_tables2.export.views import ExportMixin except ImproperlyConfigured: TableExport = None NAMES = [("Yildiz", "van der Kuil"), ("Lindi", "Hakvoort"), ("Gerardo", "Castelein")] NAMES_LIST_OF_DICTS = [ {"first_name": first_name, "last_name": last_name} for first_name, last_name in NAMES ] CSV_SEP = "\r\n" EXPECTED_CSV_DATA = tuple(",".join(name) for name in NAMES) EXPECTED_CSV = CSV_SEP.join(("First name,Surname",) + EXPECTED_CSV_DATA) + CSV_SEP EXPECTED_JSON = list( [{"First name": first_name, "Surname": last_name} for first_name, last_name in NAMES] ) class Table(tables.Table): first_name = tables.Column() last_name = tables.Column() class AccessorTable(tables.Table): given_name = tables.Column(accessor=tables.A("first_name"), verbose_name="Given name") surname = tables.Column(accessor=tables.A("last_name")) class View(ExportMixin, tables.SingleTableView): table_class = Table table_pagination = {"per_page": 1} model = Person # required for ListView template_name = "django_tables2/bootstrap.html" @skipIf(TableExport is None, "Tablib is required to run the export tests") class TableExportTest(TestCase): """ github issue #474: null/None values in exports """ def test_None_values(self): table = Table( [ {"first_name": "Yildiz", "last_name": "van der Kuil"}, {"first_name": "Jan", "last_name": None}, ] ) exporter = TableExport("csv", table) expected = ("First name,Last name", "Yildiz,van der Kuil", "Jan,") self.assertEqual(exporter.export(), CSV_SEP.join(expected) + CSV_SEP) def test_null_values(self): Person.objects.create(first_name="Jan", last_name="Coen") class Table(tables.Table): first_name = tables.Column() last_name = tables.Column(verbose_name="Last name") occupation = tables.Column(verbose_name="Occupation") table = Table(Person.objects.all()) exporter = TableExport("csv", table) expected = ("First name,Last name,Occupation", "Jan,Coen,") self.assertEqual(exporter.export(), CSV_SEP.join(expected) + CSV_SEP) def test_export_accessors_list_of_dicts(self): table = AccessorTable(NAMES_LIST_OF_DICTS) exporter = TableExport("csv", table) expected = ("Given name,Surname",) + EXPECTED_CSV_DATA self.assertEqual(exporter.export(), CSV_SEP.join(expected) + CSV_SEP) def test_export_accessors_queryset(self): programmer = Occupation.objects.create(name="Programmer") for first_name, last_name in NAMES: Person.objects.create(first_name=first_name, last_name=last_name, occupation=programmer) class AccessorRelationTable(AccessorTable): occupation = tables.Column( accessor=tables.A("occupation.name"), verbose_name="Occupation" ) table = AccessorRelationTable(Person.objects.all()) exporter = TableExport("csv", table) expected = ("Given name,Surname,Occupation",) + tuple( row + "," + programmer.name for row in EXPECTED_CSV_DATA ) self.assertEqual(exporter.export(), CSV_SEP.join(expected) + CSV_SEP) @skipIf(TableExport is None, "Tablib is required to run the export tests") class ExportViewTest(TestCase): def setUp(self): for first_name, last_name in NAMES: Person.objects.create(first_name=first_name, last_name=last_name) def test_view_should_support_csv_export(self): response = View.as_view()(build_request("/?_export=csv")) self.assertEqual(response.getvalue().decode("utf8"), EXPECTED_CSV) # should just render the normal table without the _export query response = View.as_view()(build_request("/")) html = response.render().rendered_content self.assertIn("Yildiz", html) self.assertNotIn("Lindy", html) def test_should_raise_error_for_unsupported_file_type(self): table = Table([]) with self.assertRaises(TypeError): TableExport(table=table, export_format="exe") def test_should_support_json_export(self): response = View.as_view()(build_request("/?_export=json")) self.assertEqual(json.loads(response.getvalue().decode("utf8")), EXPECTED_JSON) def test_should_support_custom_trigger_param(self): class View(ExportMixin, tables.SingleTableView): table_class = Table export_trigger_param = "export_to" model = Person # required for ListView response = View.as_view()(build_request("/?export_to=json")) self.assertEqual(json.loads(response.getvalue().decode("utf8")), EXPECTED_JSON) def test_should_support_custom_filename(self): class View(ExportMixin, tables.SingleTableView): table_class = Table export_name = "people" model = Person # required for ListView response = View.as_view()(build_request("/?_export=json")) self.assertEqual(response["Content-Disposition"], 'attachment; filename="people.json"') def test_function_view(self): """Test the code used in the docs.""" def table_view(request): table = Table(Person.objects.all()) RequestConfig(request).configure(table) export_format = request.GET.get("_export", None) if TableExport.is_valid_format(export_format): exporter = TableExport(export_format, table) return exporter.response("table.{}".format(export_format)) return render(request, "django_tables2/table.html", {"table": table}) response = table_view(build_request("/?_export=csv")) self.assertEqual(response.getvalue().decode("utf8"), EXPECTED_CSV) # must also support the normal html table. response = table_view(build_request("/")) html = response.content.decode("utf8") self.assertIn("Yildiz", html) self.assertNotIn("Lindy", html) class OccupationTable(tables.Table): name = tables.Column() boolean = tables.Column() region = tables.Column() class OccupationView(ExportMixin, tables.SingleTableView): model = Occupation table_class = OccupationTable table_pagination = {"per_page": 1} template_name = "django_tables2/bootstrap.html" @skipIf(TableExport is None, "Tablib is required to run the export tests") class AdvancedExportViewTest(TestCase): @classmethod def setUpClass(cls): super().setUpClass() richard = Person.objects.create(first_name="Richard", last_name="Queener") vlaanderen = Region.objects.create(name="Vlaanderen", mayor=richard) Occupation.objects.create(name="Timmerman", boolean=True, region=vlaanderen) Occupation.objects.create(name="Ecoloog", boolean=False, region=vlaanderen) def test_should_work_with_foreign_keys(self): response = OccupationView.as_view()(build_request("/?_export=xls")) data = response.content # binary data, so not possible to compare to an exact expectation self.assertTrue(data.find("Vlaanderen".encode())) self.assertTrue(data.find("Ecoloog".encode())) self.assertTrue(data.find("Timmerman".encode())) def test_datetime_xls(self): """Verify datatime objects can be exported to xls.""" utc = pytz.timezone("UTC") class Table(tables.Table): date = tables.DateColumn() time = tables.TimeColumn() datetime = tables.DateTimeColumn() class View(ExportMixin, tables.SingleTableView): table_class = Table table_pagination = {"per_page": 1} template_name = "django_tables2/bootstrap.html" def get_queryset(self): return [ { "date": date(2019, 7, 22), "time": time(11, 11, 11), "datetime": utc.localize(datetime(2019, 7, 22, 11, 11, 11)), } ] response = View.as_view()(build_request("/?_export=csv")) data = response.getvalue().decode("utf8") expected_csv = "\r\n".join( ("Date,Time,Datetime", "2019-07-22,11:11:11,2019-07-22 13:11:11", "") ) self.assertEqual(data, expected_csv) response = View.as_view()(build_request("/?_export=xls")) self.assertIn("2019-07-22 13:11:11".encode(), response.content) def test_export_invisible_columns(self): """Verify columns with visible=False *do* get exported.""" DATA = [{"name": "Bess W. Fletcher", "website": "teammonka.com"}] class Table(tables.Table): name = tables.Column() website = tables.Column(visible=False) class View(ExportMixin, tables.SingleTableView): table_class = Table table_pagination = {"per_page": 1} template_name = "django_tables2/bootstrap.html" def get_queryset(self): return DATA response = View.as_view()(build_request()) self.assertNotContains(response, "teammonka.com") response = View.as_view()(build_request("/?_export=csv")) data = response.getvalue().decode() expected_csv = "\r\n".join(("Name,Website", "Bess W. Fletcher,teammonka.com", "")) self.assertEqual(data, expected_csv) def test_should_work_with_foreign_key_fields(self): class OccupationWithForeignKeyFieldsTable(tables.Table): name = tables.Column() boolean = tables.Column() region = tables.Column() mayor = tables.Column(accessor="region.mayor.first_name") class View(ExportMixin, tables.SingleTableView): table_class = OccupationWithForeignKeyFieldsTable table_pagination = {"per_page": 1} model = Occupation template_name = "django_tables2/bootstrap.html" response = View.as_view()(build_request("/?_export=csv")) data = response.getvalue().decode("utf8") expected_csv = "\r\n".join( ( "Name,Boolean,Region,First name", "Timmerman,True,Vlaanderen,Richard", "Ecoloog,False,Vlaanderen,Richard", "", ) ) self.assertEqual(data, expected_csv) def test_should_allow_exclude_columns(self): class OccupationExcludingView(ExportMixin, tables.SingleTableView): table_class = OccupationTable table_pagination = {"per_page": 1} model = Occupation template_name = "django_tables2/bootstrap.html" exclude_columns = ("boolean",) response = OccupationExcludingView.as_view()(build_request("/?_export=csv")) data = response.getvalue().decode("utf8") self.assertEqual(data.splitlines()[0], "Name,Region") @skipIf(TableExport is None, "Tablib is required to run the export tests") class UnicodeExportViewTest(TestCase): def test_exporting_unicode_data(self): unicode_name = "木匠" Occupation.objects.create(name=unicode_name) expected_csv = "Name,Boolean,Region\r\n{},,\r\n".format(unicode_name) response = OccupationView.as_view()(build_request("/?_export=csv")) self.assertEqual(response.getvalue().decode("utf8"), expected_csv) # smoke tests, hard to test this binary format for string containment response = OccupationView.as_view()(build_request("/?_export=xls")) self.assertGreater(len(response.content), len(expected_csv)) response = OccupationView.as_view()(build_request("/?_export=xlsx")) self.assertGreater(len(response.content), len(expected_csv)) def test_exporting_unicode_header(self): unicode_header = "hé" class Table(tables.Table): name = tables.Column(verbose_name=unicode_header) exporter = TableExport("csv", Table([])) response = exporter.response() self.assertEqual(response.getvalue().decode("utf8"), unicode_header + "\r\n") exporter = TableExport("xls", Table([])) # this would fail if the header contains unicode and string converstion is attempted. exporter.export() django-tables2-2.1.1/tests/test_paginators.py0000644000175000017500000000453613542204315020602 0ustar josephjosephfrom django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.test import TestCase from django_tables2 import LazyPaginator class FakeQuerySet: objects = range(1, 10 ** 6) def count(self): raise AssertionError("LazyPaginator should not call QuerySet.count()") def __getitem__(self, key): return self.objects[key] def __iter__(self): yield next(self.objects) class LazyPaginatorTest(TestCase): def test_compare_to_default_paginator(self): objects = list(range(1, 1000)) paginator = Paginator(objects, 10) lazy_paginator = LazyPaginator(objects, 10) self.assertEqual(paginator.page(1).object_list, lazy_paginator.page(1).object_list) self.assertEqual(paginator.page(10).object_list, lazy_paginator.page(10).object_list) self.assertEqual(paginator.page(100).object_list, lazy_paginator.page(100).object_list) def test_no_count_call(self): paginator = LazyPaginator(FakeQuerySet(), 10) # num_pages initially is None, but is page_number + 1 after requesting a page. self.assertEqual(paginator.num_pages, None) paginator.page(1) self.assertEqual(paginator.num_pages, 2) paginator.page(3) self.assertEqual(paginator.num_pages, 4) paginator.page(1.0) # and again decreases when a lower page nu self.assertEqual(paginator.num_pages, 2) with self.assertRaises(PageNotAnInteger): paginator.page(1.5) with self.assertRaises(EmptyPage): paginator.page(-1) with self.assertRaises(NotImplementedError): paginator.count() with self.assertRaises(NotImplementedError): paginator.page_range() # last page last_page_number = 10 ** 5 paginator.page(last_page_number) with self.assertRaises(EmptyPage): paginator.page(last_page_number + 1) def test_lookahead(self): objects = list(range(1, 1000)) paginator = LazyPaginator(objects, 10, look_ahead=3) self.assertEqual(paginator.look_ahead, 3) self.assertEqual(paginator.page(1).object_list, list(range(1, 11))) self.assertEqual(paginator.num_pages, 4) self.assertEqual(paginator.page(98).object_list, list(range(971, 981))) self.assertEqual(paginator.num_pages, 100) django-tables2-2.1.1/tests/test_pinned_rows.py0000644000175000017500000001343413542204315020757 0ustar josephjosephfrom django.test import SimpleTestCase import django_tables2 as tables from django_tables2.rows import BoundRow, BoundRows from .utils import build_request, parse class PinnedObj: def __init__(self, name, age): self.name = name self.age = age class SimpleTable(tables.Table): name = tables.Column() occupation = tables.Column() age = tables.Column() def get_top_pinned_data(self): return [PinnedObj("Ron", 90), PinnedObj("Jon", 10)] def get_bottom_pinned_data(self): return [{"occupation": "Sum age", "age": 130}] class PinnedRowsTest(SimpleTestCase): def test_bound_rows_with_pinned_data(self): record = {"name": "Grzegorz", "age": 30, "occupation": "programmer"} table = SimpleTable([record]) self.assertEqual(len(table.rows), 4) # rows + pinned data row = table.rows[0] with self.assertRaises(IndexError): table.rows[1] with self.assertRaises(IndexError): row.get_cell(3) self.assertEqual(row.get_cell("name"), record["name"]) self.assertEqual(row.get_cell("occupation"), record["occupation"]) self.assertEqual(row.get_cell("age"), record["age"]) with self.assertRaises(KeyError): row.get_cell("gamma") self.assertIn("name", row) self.assertIn("occupation", row) self.assertNotIn("gamma", row) def test_as_html(self): """ Ensure that html render correctly. """ request = build_request("/") table = SimpleTable([{"name": "Grzegorz", "age": 30, "occupation": "programmer"}]) root = parse(table.as_html(request)) # One row for header self.assertEqual(len(root.findall(".//thead/tr")), 1) # In the header should be 3 cell. self.assertEqual(len(root.findall(".//thead/tr/th")), 3) # In the body, should be one original record and 3 pinned rows. self.assertEqual(len(root.findall(".//tbody/tr")), 4) self.assertEqual(len(root.findall(".//tbody/tr/td")), 12) # First top pinned row. tr = root.findall(".//tbody/tr") td = tr[0].findall("td") self.assertEqual(td[0].text, "Ron") self.assertEqual(td[1].text, table.default) self.assertEqual(td[2].text, "90") # Second top pinned row. td = tr[1].findall("td") self.assertEqual(td[0].text, "Jon") self.assertEqual(td[1].text, table.default) self.assertEqual(td[2].text, "10") # Original row td = tr[2].findall("td") self.assertEqual(td[0].text, "Grzegorz") self.assertEqual(td[1].text, "programmer") self.assertEqual(td[2].text, "30") # First bottom pinned row. td = tr[3].findall("td") self.assertEqual(td[0].text, table.default) self.assertEqual(td[1].text, "Sum age") self.assertEqual(td[2].text, "130") def test_pinned_row_attrs(self): """ Testing attrs for pinned rows. """ pinned_row_attrs = {"class": "super-mega-row", "data-foo": "bar"} request = build_request("/") record = {"name": "Grzegorz", "age": 30, "occupation": "programmer"} table = SimpleTable([record], pinned_row_attrs=pinned_row_attrs) html = table.as_html(request) self.assertIn("pinned-row", html) self.assertIn("super-mega-row", html) self.assertIn("data-foo", html) def test_ordering(self): """ Change sorting should not change ordering pinned rows. """ request = build_request("/") records = [ {"name": "Alex", "age": 42, "occupation": "programmer"}, {"name": "Greg", "age": 30, "occupation": "programmer"}, ] table = SimpleTable(records, order_by="age") root = parse(table.as_html(request)) tr = root.findall(".//tbody/tr") self.assertEqual(tr[0].findall("td")[2].text, "90") self.assertEqual(tr[1].findall("td")[2].text, "10") self.assertEqual(tr[2].findall("td")[2].text, "30") self.assertEqual(tr[3].findall("td")[2].text, "42") self.assertEqual(tr[4].findall("td")[2].text, "130") table = SimpleTable(records, order_by="-age") root = parse(table.as_html(request)) tr = root.findall(".//tbody/tr") self.assertEqual(tr[0].findall("td")[2].text, "90") self.assertEqual(tr[1].findall("td")[2].text, "10") self.assertEqual(tr[2].findall("td")[2].text, "42") self.assertEqual(tr[3].findall("td")[2].text, "30") self.assertEqual(tr[4].findall("td")[2].text, "130") def test_bound_rows_getitem(self): """ Testing BoundRows.__getitem__() method. Checking the return class for simple value and for slice. Ensure that inside of BoundRows pinned rows are included in length. """ records = [ {"name": "Greg", "age": 30, "occupation": "policeman"}, {"name": "Alex", "age": 42, "occupation": "programmer"}, {"name": "John", "age": 72, "occupation": "official"}, ] table = SimpleTable(records, order_by="age") self.assertIsInstance(table.rows[0], BoundRow) self.assertIsInstance(table.rows[0:2], BoundRows) self.assertEqual(table.rows[0:2][0].get_cell("name"), "Greg") self.assertEqual(len(table.rows[:]), 6) def test_uniterable_pinned_data(self): """ Ensure that, when data for pinned rows are not iterable, the ValueError exception will be raised. """ class FooTable(tables.Table): col = tables.Column() def get_top_pinned_data(self): return 1 tab = FooTable([1, 2, 3]) with self.assertRaises(ValueError): for row in tab.rows: pass django-tables2-2.1.1/tests/test_models.py0000644000175000017500000005146113542204315017715 0ustar josephjosephfrom collections import defaultdict from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models.functions import Length from django.template import Context, Template from django.test import TestCase from django.utils.translation import override as translation_override import django_tables2 as tables import mock from .app.models import Occupation, Person, PersonProxy from .utils import build_request, parse request = build_request() class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.Column() occupation = tables.Column() class ModelsTest(TestCase): def setUp(self): occupation = Occupation.objects.create(name="Programmer") Person.objects.create(first_name="Bradley", last_name="Ayers", occupation=occupation) Person.objects.create(first_name="Chris", last_name="Doble", occupation=occupation) def test_check_types_model(self): class Abstract(models.Model): name = models.CharField(max_length=100) class Meta: abstract = True app_label = "django_tables2_test" class Concrete(Abstract): pass def test(Model): class Table(tables.Table): class Meta: model = Model return Table([]) valid = [ Abstract, Concrete, Occupation, Person, PersonProxy, ContentType.objects.get(model="person").model_class(), ] invalid = [object, {}, dict] for Model in valid: test(Model) for Model in invalid: with self.assertRaises(TypeError): test(Model) def test_boundrows_iteration(self): table = PersonTable(Person.objects.all()) expected = list(Person.objects.all()) for i, actual in enumerate([row.record for row in table.rows]): self.assertEqual(expected[i], actual) def test_should_support_rendering_multiple_times(self): class MultiRenderTable(tables.Table): name = tables.Column() # test queryset data table = MultiRenderTable(Person.objects.all()) self.assertEqual(table.as_html(request), table.as_html(request)) def test_doesnotexist_from_accessor_should_use_default(self): class Table(tables.Table): class Meta: model = Person default = "abc" fields = ("first_name", "last_name", "region") table = Table(Person.objects.all()) self.assertEqual(table.rows[0].get_cell("first_name"), "Bradley") self.assertEqual(table.rows[0].get_cell("region"), "abc") def test_unicode_field_names(self): class Table(tables.Table): class Meta: model = Person fields = (str("first_name"),) table = Table(Person.objects.all()) self.assertEqual(table.rows[0].get_cell("first_name"), "Bradley") def test_Meta_option_model_table(self): """ The ``model`` option on a table causes the table to dynamically add columns based on the fields. """ class OccupationTable(tables.Table): class Meta: model = Occupation expected = ["id", "name", "region", "boolean", "boolean_with_choices"] self.assertEqual(expected, list(OccupationTable.base_columns.keys())) class OccupationTable2(tables.Table): extra = tables.Column() class Meta: model = Occupation expected.append("extra") self.assertEqual(expected, list(OccupationTable2.base_columns.keys())) # be aware here, we already have *models* variable, but we're importing # over the top from django.db import models class ComplexModel(models.Model): char = models.CharField(max_length=200) fk = models.ForeignKey("self", on_delete=models.CASCADE) m2m = models.ManyToManyField("self") class Meta: app_label = "django_tables2_test" class ComplexTable(tables.Table): class Meta: model = ComplexModel self.assertEqual(["id", "char", "fk"], list(ComplexTable.base_columns.keys())) def test_mixins(self): class TableMixin(tables.Table): extra = tables.Column() class OccupationTable(TableMixin, tables.Table): extra2 = tables.Column() class Meta: model = Occupation self.assertEqual( list(OccupationTable.base_columns.keys()), ["extra", "id", "name", "region", "boolean", "boolean_with_choices", "extra2"], ) def test_fields_empty_list_means_no_fields(self): class Table(tables.Table): class Meta: model = Person fields = () table = Table(Person.objects.all()) self.assertEqual(len(table.columns.names()), 0) def test_compound_ordering(self): class SimpleTable(tables.Table): name = tables.Column(order_by=("first_name", "last_name")) table = SimpleTable(Person.objects.all(), order_by="name") html = table.as_html(request) self.assertEqual(parse(html).findall(".//thead/tr/th/a")[0].attrib, {"href": "?sort=-name"}) def test_default_order(self): """ If orderable=False, do not sort queryset. https://github.com/bradleyayers/django-tables2/issues/204 """ class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.Column() table = PersonTable(PersonProxy.objects.all()) table.data.order_by([]) self.assertEqual(list(table.rows[0]), ["Bradley", "Ayers"]) def test_fields_should_implicitly_set_sequence(self): class PersonTable(tables.Table): extra = tables.Column() class Meta: model = Person fields = ("last_name", "first_name") table = PersonTable(Person.objects.all()) self.assertEqual(table.columns.names(), ["last_name", "first_name", "extra"]) def test_model_properties_should_be_useable_for_columns(self): class PersonTable(tables.Table): class Meta: model = Person fields = ("name", "first_name") table = PersonTable(Person.objects.all()) self.assertEqual(list(table.rows[0]), ["Bradley Ayers", "Bradley"]) def test_meta_fields_may_be_list(self): class PersonTable(tables.Table): class Meta: model = Person fields = ["name", "first_name"] table = PersonTable(Person.objects.all()) self.assertEqual(list(table.rows[0]), ["Bradley Ayers", "Bradley"]) class ColumnNameTest(TestCase): def setUp(self): for i in range(10): Person.objects.create(first_name="Bob %d" % i, last_name="Builder") def test_column_verbose_name(self): """ When using queryset data as input for a table, default to using model field verbose names rather than an autogenerated string based on the column name. However if a column does explicitly describe a verbose name, it should be used. """ class PersonTable(tables.Table): """ The test_colX columns are to test that the accessor is used to determine the field on the model, rather than the column name. """ first_name = tables.Column() fn1 = tables.Column(accessor="first_name") fn2 = tables.Column(accessor="first_name.upper") fn3 = tables.Column(accessor="last_name", verbose_name="OVERRIDE") fn4 = tables.Column(accessor="last_name", verbose_name="override") last_name = tables.Column() ln1 = tables.Column(accessor="last_name") ln2 = tables.Column(accessor="last_name.upper") ln3 = tables.Column(accessor="last_name", verbose_name="OVERRIDE") region = tables.Column(accessor="occupation.region.name") r1 = tables.Column(accessor="occupation.region.name") r2 = tables.Column(accessor="occupation.region.name.upper") r3 = tables.Column(accessor="occupation.region.name", verbose_name="OVERRIDE") trans_test = tables.Column() trans_test_lazy = tables.Column() # The Person model has a ``first_name`` and ``last_name`` field, but only # the ``last_name`` field has an explicit ``verbose_name`` set. This means # that we should expect that the two columns that use the ``last_name`` # field should both use the model's ``last_name`` field's ``verbose_name``, # however both fields that use the ``first_name`` field should just use a # titlised version of the column name as the column header. table = PersonTable(Person.objects.all()) # Should be generated (capitalized column name) self.assertEqual("First name", table.columns["first_name"].verbose_name) self.assertEqual("First name", table.columns["fn1"].verbose_name) self.assertEqual("First name", table.columns["fn2"].verbose_name) self.assertEqual("OVERRIDE", table.columns["fn3"].verbose_name) self.assertEqual("override", table.columns["fn4"].verbose_name) # Should use the titlised model field's verbose_name self.assertEqual("Surname", table.columns["last_name"].verbose_name) self.assertEqual("Surname", table.columns["ln1"].verbose_name) self.assertEqual("Surname", table.columns["ln2"].verbose_name) self.assertEqual("OVERRIDE", table.columns["ln3"].verbose_name) self.assertEqual("Name", table.columns["region"].verbose_name) self.assertEqual("Name", table.columns["r1"].verbose_name) self.assertEqual("Name", table.columns["r2"].verbose_name) self.assertEqual("OVERRIDE", table.columns["r3"].verbose_name) self.assertEqual("Translation test", table.columns["trans_test"].verbose_name) self.assertEqual("Translation test lazy", table.columns["trans_test_lazy"].verbose_name) def test_using_Meta_model(self): # Now we'll try using a table with Meta.model class PersonTable(tables.Table): first_name = tables.Column(verbose_name="OVERRIDE") class Meta: model = Person # Issue #16 table = PersonTable(Person.objects.all()) self.assertEqual("Translation test", table.columns["trans_test"].verbose_name) self.assertEqual("Translation test lazy", table.columns["trans_test_lazy"].verbose_name) self.assertEqual("Web site", table.columns["website"].verbose_name) self.assertEqual("Birthdate", table.columns["birthdate"].verbose_name) self.assertEqual("OVERRIDE", table.columns["first_name"].verbose_name) class PersonTable(tables.Table): class Meta: model = Person table = PersonTable(Person.objects.all()) with translation_override("ua"): self.assertEqual( "Тест ленивого перекладу", table.columns["trans_test_lazy"].verbose_name ) def test_data_verbose_name(self): table = tables.Table(Person.objects.all()) self.assertEqual(table.data.verbose_name, "person") self.assertEqual(table.data.verbose_name_plural, "people") def test_column_named_delete(self): class DeleteTable(tables.Table): delete = tables.TemplateColumn("[delete button]", verbose_name="") class Meta: model = Person fields = ("name", "delete") person1 = Person.objects.create(first_name="Jan", last_name="Pieter") person2 = Person.objects.create(first_name="John", last_name="Peter") DeleteTable(Person.objects.all()).as_html(build_request()) self.assertEqual(Person.objects.get(pk=person1.pk), person1) self.assertEqual(Person.objects.get(pk=person2.pk), person2) class ModelFieldTest(TestCase): def test_use_to_translated_value(self): """ When a model field uses the ``choices`` option, a table should render the 'pretty' value rather than the database value. See issue #30 for details. """ LANGUAGES = (("en", "English"), ("ru", "Russian")) from django.db import models class Article(models.Model): name = models.CharField(max_length=200) language = models.CharField(max_length=200, choices=LANGUAGES) class Meta: app_label = "tests" def __unicode__(self): return self.name class ArticleTable(tables.Table): class Meta: model = Article table = ArticleTable( [ Article(name="English article", language="en"), Article(name="Russian article", language="ru"), ] ) self.assertEqual("English", table.rows[0].get_cell("language")) self.assertEqual("Russian", table.rows[1].get_cell("language")) def test_column_mapped_to_nonexistant_field(self): """ Issue #9 describes how if a Table has a column that has an accessor that targets a non-existent field, a FieldDoesNotExist error is raised. """ class FaultyPersonTable(PersonTable): missing = tables.Column() table = FaultyPersonTable(Person.objects.all()) table.as_html(request) # the bug would cause this to raise FieldDoesNotExist class OrderingDataTest(TestCase): NAMES = ("Bradley Ayers", "Stevie Armstrong", "VeryLongFirstName VeryLongLastName") @classmethod def setUpClass(cls): super().setUpClass() for name in cls.NAMES: first_name, last_name = name.split() Person.objects.create(first_name=first_name, last_name=last_name) def test_order_by_derived_from_queryset(self): queryset = Person.objects.order_by("first_name", "last_name", "-occupation__name") class PersonTable(tables.Table): name = tables.Column(order_by=("first_name", "last_name")) occupation = tables.Column(order_by=("occupation__name",)) self.assertEqual( PersonTable(queryset.order_by("first_name", "last_name", "-occupation__name")).order_by, ("name", "-occupation"), ) class PersonTable(PersonTable): class Meta: order_by = ("occupation",) self.assertEqual(PersonTable(queryset.all()).order_by, ("occupation",)) def test_queryset_table_data_supports_ordering(self): class Table(tables.Table): class Meta: model = Person table = Table(Person.objects.all()) self.assertEqual(table.rows[0].get_cell("first_name"), "Bradley") table.order_by = "-first_name" self.assertEqual(table.rows[0].get_cell("first_name"), "VeryLongFirstName") def test_queryset_table_data_supports_custom_ordering(self): class Table(tables.Table): class Meta: model = Person order_by = "first_name" def order_first_name(self, queryset, is_descending): # annotate to order by length of first_name + last_name queryset = queryset.annotate( length=Length("first_name") + Length("last_name") ).order_by(("-" if is_descending else "") + "length") return (queryset, True) table = Table(Person.objects.all()) # Shortest full names first self.assertEqual(table.rows[0].get_cell("first_name"), "Bradley") # Longest full names first table.order_by = "-first_name" self.assertEqual(table.rows[0].get_cell("first_name"), "VeryLongFirstName") class ModelSanityTest(TestCase): @classmethod def setUpClass(cls): super().setUpClass() for i in range(10): Person.objects.create(first_name="Bob %d" % i, last_name="Builder") def test_column_with_delete_accessor_shouldnt_delete_records(self): class PersonTable(tables.Table): delete = tables.Column() table = PersonTable(Person.objects.all()) table.as_html(request) self.assertEqual(Person.objects.all().count(), 10) def test_model__str__calls(self): """ Model.__str__ should not be called when not necessary. """ calls = defaultdict(int) def counting__str__(self): calls[self.pk] += 1 return self.first_name with mock.patch("tests.app.models.Person.__str__", counting__str__): for i in range(1, 4): Person.objects.create(first_name="Bob %d" % i, last_name="Builder") class PersonTable(tables.Table): edit = tables.Column() class Meta: model = Person fields = ["first_name", "last_name"] self.assertEqual(calls, {}) table = PersonTable(Person.objects.all()) table.as_html(build_request()) self.assertEqual(calls, {}) def test_render_table_template_tag_numqueries(self): class PersonTable(tables.Table): class Meta: model = Person per_page = 1 request = build_request("/") with self.assertNumQueries(0): table = PersonTable(Person.objects.all()) with self.assertNumQueries(1): # one query for pagination: .count() tables.RequestConfig(request).configure(table) template = Template("{% load django_tables2 %}{% render_table table %}") context = Context({"table": table, "request": request}) with self.assertNumQueries(1): # one query for page records template.render(context) with self.assertNumQueries(0): # re-render should not produce extra queries template.render(context) # second page request = build_request("/?page=2") context = Context({"table": table, "request": request}) with self.assertNumQueries(0): # count is already done, not needed anymore tables.RequestConfig(request).configure(table) with self.assertNumQueries(1): # one query for page records template.render(context) def test_single_query_for_non_paginated_table(self): """ A non-paginated table should not generate a query for each row, but only one query fetch the rows. """ class PersonTable(tables.Table): class Meta: model = Person fields = ("first_name", "last_name") order_by = ("last_name", "first_name") table = PersonTable(Person.objects.all()) with self.assertNumQueries(1): list(table.as_values()) def test_as_html_db_queries_nonpaginated(self): """ Basic tables without pagination should NOT result in a COUNT(*) being done, but only fetch the rows. """ class PersonTable(tables.Table): class Meta: model = Person with self.assertNumQueries(1): html = PersonTable(Person.objects.all()).as_html(build_request()) self.assertIn("Bob 0", html) class TableFactoryTest(TestCase): def test_factory(self): occupation = Occupation.objects.create(name="Programmer") Person.objects.create(first_name="Bradley", last_name="Ayers", occupation=occupation) persons = Person.objects.all() Table = tables.table_factory(Person) table = Table(persons) self.assertIsInstance(table, tables.Table) self.assertEqual(Table.__name__, "PersonAutogeneratedTable") def test_factory_fields_argument(self): fields = ("username",) Table = tables.table_factory(Person, fields=fields) self.assertEqual(Table.Meta.fields, fields) self.assertEqual(Table._meta.fields, fields) def test_factory_exclude_argument(self): exclude = ("username",) Table = tables.table_factory(Person, exclude=exclude) self.assertEqual(Table.Meta.exclude, exclude) self.assertEqual(Table._meta.exclude, exclude) def test_factory_localize_argument(self): localize = ("username",) Table = tables.table_factory(Person, localize=localize) self.assertEqual(Table.Meta.localize, localize) self.assertEqual(Table._meta.localize, localize) def test_factory_with_meta(self): fields = ("first_name",) class TableWithMeta(tables.Table): first_name = tables.Column() class Meta: fields = ("first_name",) Table = tables.table_factory(Person, table=TableWithMeta) self.assertEqual(Table.Meta.fields, fields) django-tables2-2.1.1/tests/test_templatetags.py0000644000175000017500000003110213542204315021112 0ustar josephjosephfrom urllib.parse import parse_qs from django.core.exceptions import ImproperlyConfigured from django.core.paginator import Paginator from django.template import Context, RequestContext, Template, TemplateSyntaxError from django.test import SimpleTestCase, TestCase, override_settings from django_tables2 import LazyPaginator, RequestConfig, Table, TemplateColumn from django_tables2.export import ExportMixin from django_tables2.templatetags.django_tables2 import table_page_range from django_tables2.utils import AttributeDict from .app.models import Region from .test_templates import MEMORY_DATA, CountryTable from .utils import build_request, parse class RenderTableTagTest(TestCase): def test_invalid_type(self): template = Template("{% load django_tables2 %}{% render_table table %}") with self.assertRaises(ValueError): template.render(Context({"request": build_request(), "table": dict()})) def test_basic(self): request = build_request("/") # ensure it works with a multi-order-by table = CountryTable(MEMORY_DATA, order_by=("name", "population")) RequestConfig(request).configure(table) template = Template("{% load django_tables2 %}{% render_table table %}") html = template.render(Context({"request": request, "table": table})) root = parse(html) self.assertEqual(len(root.findall(".//thead/tr")), 1) self.assertEqual(len(root.findall(".//thead/tr/th")), 4) self.assertEqual(len(root.findall(".//tbody/tr")), 4) self.assertEqual(len(root.findall(".//tbody/tr/td")), 16) def test_does_not_mutate_context(self): """ Make sure the tag does not change the context of the template the tag is called from https://github.com/jieter/django-tables2/issues/547 """ class MyTable(Table): col = TemplateColumn(template_code="{{ value }}") table = MyTable([{"col": "foo"}, {"col": "bar"}], template_name="minimal.html") template = Template( "{% load django_tables2 %}" '{% with "foo" as table %}{{ table }}{% render_table mytable %}\n{{ table }}{% endwith %}' ) html = template.render(Context({"request": build_request(), "mytable": table})) lines = html.splitlines() self.assertEqual(lines[0], "foo") self.assertEqual(lines[-1], "foo") def test_table_context_is_RequestContext(self): class MyTable(Table): col = TemplateColumn(template_code="{{ value }}") template = Template("{% load django_tables2 %}{% render_table table %}") html = template.render( Context({"request": build_request(), "table": MyTable([], template_name="csrf.html")}) ) input_tag = parse(html) self.assertEqual(input_tag.get("type"), "hidden") self.assertEqual(input_tag.get("name"), "csrfmiddlewaretoken") self.assertEqual(len(input_tag.get("value")), 64) def test_no_data_without_empty_text(self): table = CountryTable([]) template = Template("{% load django_tables2 %}{% render_table table %}") html = template.render(Context({"request": build_request("/"), "table": table})) root = parse(html) self.assertEqual(len(root.findall(".//thead/tr")), 1) self.assertEqual(len(root.findall(".//thead/tr/th")), 4) self.assertEqual(len(root.findall(".//tbody/tr")), 0) def test_no_data_with_empty_text(self): # no data WITH empty_text request = build_request("/") table = CountryTable([], empty_text="this table is empty") RequestConfig(request).configure(table) template = Template("{% load django_tables2 %}{% render_table table %}") html = template.render(Context({"request": request, "table": table})) root = parse(html) self.assertEqual(len(root.findall(".//thead/tr")), 1) self.assertEqual(len(root.findall(".//thead/tr/th")), 4) self.assertEqual(len(root.findall(".//tbody/tr")), 1) self.assertEqual(len(root.findall(".//tbody/tr/td")), 1) self.assertEqual( int(root.find(".//tbody/tr/td").get("colspan")), len(root.findall(".//thead/tr/th")) ) self.assertEqual(root.find(".//tbody/tr/td").text, "this table is empty") @override_settings(DEBUG=True) def test_missing_variable(self): # variable that doesn't exist (issue #8) template = Template("{% load django_tables2 %}{% render_table this_doesnt_exist %}") with self.assertRaises(ValueError): template.render(Context()) @override_settings(DEBUG=False) def test_missing_variable_debug_False(self): template = Template("{% load django_tables2 %}{% render_table this_doesnt_exist %}") # Should still be noisy with debug off with self.assertRaises(ValueError): template.render(Context()) def test_should_support_template_argument(self): table = CountryTable(MEMORY_DATA, order_by=("name", "population")) template = Template("{% load django_tables2 %}" '{% render_table table "dummy.html" %}') context = RequestContext(build_request(), {"table": table}) self.assertEqual(template.render(context), "dummy template contents\n") def test_template_argument_list(self): template = Template("{% load django_tables2 %}" "{% render_table table template_list %}") context = RequestContext( build_request(), { "table": CountryTable(MEMORY_DATA, order_by=("name", "population")), "template_list": ("dummy.html", "child/foo.html"), }, ) self.assertEqual(template.render(context), "dummy template contents\n") def test_render_table_supports_queryset(self): for name in ("Mackay", "Brisbane", "Maryborough"): Region.objects.create(name=name) template = Template("{% load django_tables2 %}{% render_table qs %}") html = template.render(Context({"qs": Region.objects.all(), "request": build_request("/")})) root = parse(html) self.assertEqual( [e.text for e in root.findall(".//thead/tr/th/a")], ["ID", "Name", "Mayor"] ) td = [[td.text for td in tr.findall("td")] for tr in root.findall(".//tbody/tr")] db = [] for region in Region.objects.all(): db.append([str(region.id), region.name, "—"]) self.assertEqual(td, db) class QuerystringTagTest(SimpleTestCase): def test_basic(self): template = Template( "{% load django_tables2 %}" '{% querystring "name"="Brad" foo.bar=value %}' ) # Should be something like: ?name=Brad&a=b&c=5&age=21 xml = template.render( Context( {"request": build_request("/?a=b&name=dog&c=5"), "foo": {"bar": "age"}, "value": 21} ) ) # Ensure it's valid XML, retrieve the URL url = parse(xml).text qs = parse_qs(url[1:]) # everything after the ? self.assertEqual(qs["name"], ["Brad"]) self.assertEqual(qs["age"], ["21"]) self.assertEqual(qs["a"], ["b"]) self.assertEqual(qs["c"], ["5"]) def test_requires_request(self): template = Template('{% load django_tables2 %}{% querystring "name"="Brad" %}') with self.assertRaises(ImproperlyConfigured): template.render(Context()) def test_supports_without(self): context = Context({"request": build_request("/?a=b&name=dog&c=5"), "a_var": "a"}) template = Template( "{% load django_tables2 %}" '{% querystring "name"="Brad" without a_var %}' ) url = parse(template.render(context)).text qs = parse_qs(url[1:]) # trim the ? self.assertEqual(set(qs.keys()), set(["name", "c"])) def test_only_without(self): context = Context({"request": build_request("/?a=b&name=dog&c=5"), "a_var": "a"}) template = Template( "{% load django_tables2 %}" '{% querystring without "a" "name" %}' ) url = parse(template.render(context)).text qs = parse_qs(url[1:]) # trim the ? self.assertEqual(set(qs.keys()), set(["c"])) def test_querystring_syntax_error(self): with self.assertRaises(TemplateSyntaxError): Template("{% load django_tables2 %}{% querystring foo= %}") def test_querystring_as_var(self): def assert_querystring_asvar(template_code, expected): template = Template( "{% load django_tables2 %}" + "{% querystring " + template_code + " %}" + "{{ varname }}" ) # Should be something like: ?name=Brad&a=b&c=5&age=21 xml = template.render( Context({"request": build_request("/?a=b"), "foo": {"bar": "age"}, "value": 21}) ) self.assertIn("", xml) qs = parse(xml).xpath(".//strong")[0].text[1:] self.assertEqual(parse_qs(qs), expected) tests = ( ('"name"="Brad" as=varname', dict(name=["Brad"], a=["b"])), ('as=varname "name"="Brad"', dict(name=["Brad"], a=["b"])), ('"name"="Brad" as=varname without "a" ', dict(name=["Brad"])), ) for argstr, expected in tests: assert_querystring_asvar(argstr, expected) def test_export_url_tag(self): class View(ExportMixin): export_trigger_param = "_do_export" template = Template('{% load django_tables2 %}{% export_url "csv" %}') html = template.render(Context({"request": build_request("?q=foo"), "view": View()})) self.assertEqual(dict(parse_qs(html[1:])), dict(parse_qs("q=foo&_do_export=csv"))) # using a template context variable and a view template = Template("{% load django_tables2 %}{% export_url format %}") html = template.render( Context({"request": build_request("?q=foo"), "format": "xls", "view": View()}) ) self.assertEqual(dict(parse_qs(html[1:])), dict(parse_qs("q=foo&_do_export=xls"))) # using a template context variable template = Template("{% load django_tables2 %}{% export_url format %}") html = template.render(Context({"request": build_request("?q=foo"), "format": "xls"})) self.assertEqual(dict(parse_qs(html[1:])), dict(parse_qs("q=foo&_export=xls"))) # using a template context and change export parameter template = Template('{% load django_tables2 %}{% export_url "xls" "_other_export_param" %}') html = template.render(Context({"request": build_request("?q=foo"), "format": "xls"})) self.assertEqual( dict(parse_qs(html[1:])), dict(parse_qs("q=foo&_other_export_param=xls")) ) def test_render_attributes_test(self): template = Template('{% load django_tables2 %}{% render_attrs attrs class="table" %}') html = template.render(Context({})) self.assertEqual(html, 'class="table"') html = template.render(Context({"attrs": AttributeDict({"class": "table table-striped"})})) self.assertEqual(html, 'class="table table-striped"') class TablePageRangeTest(SimpleTestCase): def test_table_page_range(self): paginator = Paginator(range(1, 1000), 10) self.assertEqual( table_page_range(paginator.page(1), paginator), [1, 2, 3, 4, 5, 6, 7, 8, "...", 100] ) self.assertEqual( table_page_range(paginator.page(10), paginator), [1, "...", 7, 8, 9, 10, 11, 12, "...", 100], ) self.assertEqual( table_page_range(paginator.page(100), paginator), [1, "...", 93, 94, 95, 96, 97, 98, 99, 100], ) def test_table_page_range_num_pages_equals_page_range_plus_one(self): paginator = Paginator(range(1, 11 * 10), 10) self.assertEqual( table_page_range(paginator.page(1), paginator), [1, 2, 3, 4, 5, 6, 7, 8, "...", 11] ) self.assertEqual( table_page_range(paginator.page(6), paginator), [1, 2, 3, 4, 5, 6, 7, 8, "...", 11] ) self.assertEqual( table_page_range(paginator.page(7), paginator), [1, "...", 4, 5, 6, 7, 8, 9, 10, 11] ) def test_table_page_range_lazy(self): paginator = LazyPaginator(range(1, 1000), 10) self.assertEqual(table_page_range(paginator.page(1), paginator), range(1, 3)) self.assertEqual( table_page_range(paginator.page(10), paginator), [1, "...", 4, 5, 6, 7, 8, 9, 10, 11] ) django-tables2-2.1.1/tests/test_ordering.py0000644000175000017500000002507713542204315020247 0ustar josephjosephfrom datetime import datetime from django.test import TestCase import django_tables2 as tables from django_tables2 import RequestConfig from .app.models import Person from .utils import build_request request = build_request("/") MEMORY_DATA = [ {"i": 2, "alpha": "b", "beta": "b"}, {"i": 1, "alpha": "a", "beta": "c"}, {"i": 3, "alpha": "c", "beta": "a"}, ] PEOPLE = [ {"first_name": "Bradley", "last_name": "Ayers"}, {"first_name": "Bradley", "last_name": "Fake"}, {"first_name": "Chris", "last_name": "Doble"}, {"first_name": "Davina", "last_name": "Adisusila"}, {"first_name": "Ross", "last_name": "Ayers"}, ] class UnorderedTable(tables.Table): i = tables.Column() alpha = tables.Column() beta = tables.Column() class OrderedTable(UnorderedTable): class Meta: order_by = "alpha" class OrderingTest(TestCase): def test_meta_ordering_list(self): class Table(UnorderedTable): class Meta: order_by = ["i", "alpha"] self.assertEqual(Table([]).order_by, ("i", "alpha")) self.assertEqual(Table([], order_by=["alpha", "i"]).order_by, ("alpha", "i")) def test_meta_ordering_tuple(self): class Table(UnorderedTable): class Meta: order_by = ("i", "alpha") self.assertEqual(Table([]).order_by, ("i", "alpha")) def test_ordering(self): # fallback to Table.Meta self.assertEqual(OrderedTable([], order_by=None).order_by, ("alpha",)) self.assertEqual(OrderedTable([]).order_by, ("alpha",)) # values of order_by are wrapped in tuples before being returned self.assertEqual(OrderedTable([], order_by="alpha").order_by, ("alpha",)) self.assertEqual(OrderedTable([], order_by=("beta",)).order_by, ("beta",)) for test in [[], (), ""]: table = OrderedTable([]) table.order_by = test self.assertEqual(table.order_by, ()) self.assertEqual(table.order_by, OrderedTable([], order_by=[]).order_by) # apply an ordering table = UnorderedTable([]) table.order_by = "alpha" self.assertEqual(table.order_by, ("alpha",)) self.assertEqual(UnorderedTable([], order_by="alpha").order_by, ("alpha",)) # let's check the data table = OrderedTable(MEMORY_DATA, order_by="beta") self.assertEqual(table.rows[0].get_cell("i"), 3) table = OrderedTable(MEMORY_DATA, order_by="-beta") self.assertEqual(table.rows[0].get_cell("i"), 1) # allow fallback to Table.Meta.order_by table = OrderedTable(MEMORY_DATA) self.assertEqual(table.rows[0].get_cell("i"), 1) # column's can't be ordered if they're not allowed to be class TestTable2(tables.Table): a = tables.Column(orderable=False) b = tables.Column() table = TestTable2([], order_by="a") self.assertEqual(table.order_by, ()) table = TestTable2([], order_by="b") self.assertEqual(table.order_by, ("b",)) # ordering disabled by default class TestTable3(tables.Table): a = tables.Column(orderable=True) b = tables.Column() class Meta: orderable = False table = TestTable3([], order_by="a") self.assertEqual(table.order_by, ("a",)) table = TestTable3([], order_by="b") self.assertEqual(table.order_by, ()) table = TestTable3([], orderable=True, order_by="b") self.assertEqual(table.order_by, ("b",)) def test_ordering_different_types(self): data = [ {"i": 1, "alpha": datetime.now(), "beta": [1]}, {"i": {}, "alpha": None, "beta": ""}, {"i": 2, "alpha": None, "beta": []}, ] table = OrderedTable(data) self.assertEqual(table.rows[0].get_cell("alpha"), "—") table = OrderedTable(data, order_by="i") self.assertEqual(table.rows[0].get_cell("i"), {}) table = OrderedTable(data, order_by="beta") self.assertEqual(table.rows[0].get_cell("beta"), []) def test_multi_column_ordering_by_table(self): class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.Column() brad, brad2, chris, davina, ross = PEOPLE table = PersonTable(PEOPLE, order_by=("first_name", "last_name")) self.assertEqual([brad, brad2, chris, davina, ross], [r.record for r in table.rows]) table = PersonTable(PEOPLE, order_by=("first_name", "-last_name")) self.assertEqual([brad2, brad, chris, davina, ross], [r.record for r in table.rows]) def test_multi_column_ordering_by_column(self): # let's try column order_by using multiple keys class PersonTable(tables.Table): name = tables.Column(order_by=("first_name", "last_name")) brad, brad2, chris, davina, ross = PEOPLE # add 'name' key for each person. for person in PEOPLE: person["name"] = "{p[first_name]} {p[last_name]}".format(p=person) self.assertEqual(brad["name"], "Bradley Ayers") table = PersonTable(PEOPLE, order_by="name") self.assertEqual([brad, brad2, chris, davina, ross], [r.record for r in table.rows]) table = PersonTable(PEOPLE, order_by="-name") self.assertEqual([ross, davina, chris, brad2, brad], [r.record for r in table.rows]) def test_ordering_by_custom_field(self): """ When defining a custom field in a table, as name=tables.Column() with methods to render and order render_name and order_name, sorting by this column causes an error if the custom field is not in last position. https://github.com/jieter/django-tables2/issues/413 """ Person.objects.create(first_name="Alice", last_name="Beta") Person.objects.create(first_name="Bob", last_name="Alpha") from django.db.models import F, Value from django.db.models.functions import Concat class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.Column() full_name = tables.Column() class Meta: model = Person fields = ("first_name", "last_name", "full_name") def render_full_name(self, record): return record.last_name + " " + record.first_name def order_full_name(self, queryset, is_descending): queryset = queryset.annotate( full_name=Concat(F("last_name"), Value(" "), F("first_name")) ).order_by(("-" if is_descending else "") + "full_name") return queryset, True table = PersonTable(Person.objects.all()) request = build_request("/?sort=full_name&sort=first_name") RequestConfig(request).configure(table) self.assertEqual(table.rows[0].record.first_name, "Bob") def test_list_table_data_supports_ordering(self): class Table(tables.Table): name = tables.Column() data = [{"name": "Bradley"}, {"name": "Davina"}] table = Table(data) self.assertEqual(table.rows[0].get_cell("name"), "Bradley") table.order_by = "-name" self.assertEqual(table.rows[0].get_cell("name"), "Davina") def test_ordering_non_database_data(self): class Table(tables.Table): name = tables.Column() country = tables.Column() data = [ {"name": "Adrian", "country": "Australia"}, {"name": "Adrian", "country": "Brazil"}, {"name": "Audrey", "country": "Chile"}, {"name": "Bassie", "country": "Belgium"}, ] table = Table(data, order_by=("-name", "-country")) self.assertEqual( [(row.get_cell("name"), row.get_cell("country")) for row in table.rows], [ ("Bassie", "Belgium"), ("Audrey", "Chile"), ("Adrian", "Brazil"), ("Adrian", "Australia"), ], ) def test_table_ordering_attributes(self): class Table(tables.Table): alpha = tables.Column() beta = tables.Column() table = Table( MEMORY_DATA, attrs={ "th": { "class": "custom-header-class", "_ordering": { "orderable": "sortable", "ascending": "ascend", "descending": "descend", }, } }, order_by="alpha", ) self.assertIn("sortable", table.columns[0].attrs["th"]["class"]) self.assertIn("ascend", table.columns[0].attrs["th"]["class"]) self.assertIn("custom-header-class", table.columns[1].attrs["th"]["class"]) def test_table_ordering_attributes_in_meta(self): class Table(tables.Table): alpha = tables.Column() beta = tables.Column() class Meta(OrderedTable.Meta): attrs = { "th": { "class": "custom-header-class-in-meta", "_ordering": { "orderable": "sortable", "ascending": "ascend", "descending": "descend", }, } } table = Table(MEMORY_DATA) self.assertIn("sortable", table.columns[0].attrs["th"]["class"]) self.assertIn("ascend", table.columns[0].attrs["th"]["class"]) self.assertIn("custom-header-class-in-meta", table.columns[1].attrs["th"]["class"]) def test_column_ordering_attributes(self): class Table(tables.Table): alpha = tables.Column( attrs={ "th": { "class": "custom-header-class", "_ordering": {"orderable": "sort", "ascending": "ascending"}, } } ) beta = tables.Column( attrs={"th": {"_ordering": {"orderable": "canOrder"}}, "td": {"class": "cell-2"}} ) table = Table(MEMORY_DATA, attrs={"class": "only-on-table"}, order_by="alpha") self.assertNotIn("only-on-table", table.columns[0].attrs["th"]["class"]) self.assertIn("custom-header-class", table.columns[0].attrs["th"]["class"]) self.assertIn("ascending", table.columns[0].attrs["th"]["class"]) self.assertIn("sort", table.columns[0].attrs["th"]["class"]) self.assertIn("canOrder", table.columns[1].attrs["th"]["class"]) django-tables2-2.1.1/tests/utils.py0000644000175000017500000000263713542204315016534 0ustar josephjosephfrom io import StringIO import lxml.etree import lxml.html from django.core.handlers.wsgi import WSGIRequest from django.test.client import FakePayload def parse(html): # We use html instead of etree. Because etree can't parse html entities. return lxml.html.fromstring(html) def attrs(xml): """ Helper function that returns a dict of XML attributes, given an element. """ return lxml.html.fromstring(xml).attrib def build_request(uri="/", user=None): """ Return a fresh HTTP GET / request. This is essentially a heavily cutdown version of Django's `~django.test.client.RequestFactory`. """ path, _, querystring = uri.partition("?") request = WSGIRequest( { "CONTENT_TYPE": "text/html; charset=utf-8", "PATH_INFO": path, "QUERY_STRING": querystring, "REMOTE_ADDR": "127.0.0.1", "REQUEST_METHOD": "GET", "SCRIPT_NAME": "", "SERVER_NAME": "testserver", "SERVER_PORT": "80", "SERVER_PROTOCOL": "HTTP/1.1", "wsgi.version": (1, 0), "wsgi.url_scheme": "http", "wsgi.input": FakePayload(b""), "wsgi.errors": StringIO(), "wsgi.multiprocess": True, "wsgi.multithread": False, "wsgi.run_once": False, } ) if user is not None: request.user = user return request django-tables2-2.1.1/tests/app/0000755000175000017500000000000013542204315015572 5ustar josephjosephdjango-tables2-2.1.1/tests/app/views.py0000644000175000017500000000105413542204315017301 0ustar josephjosephfrom django.http import HttpResponse from django.shortcuts import get_object_or_404 from .models import Occupation, Person def person(request, pk): """A really simple view to provide an endpoint for the 'person' URL.""" person = get_object_or_404(Person, pk=pk) return HttpResponse("Person: %s" % person) def occupation(request, pk): """ Another really simple view to provide an endpoint for the 'occupation' URL. """ occupation = get_object_or_404(Occupation, pk=pk) return HttpResponse("Occupation: %s" % occupation) django-tables2-2.1.1/tests/app/urls.py0000644000175000017500000000066213542204315017135 0ustar josephjosephfrom django.conf.urls import url from . import views urlpatterns = [ url(r"^people/delete/(?P\d+)/$", views.person, name="person_delete"), url(r"^people/edit/(?P\d+)/$", views.person, name="person_edit"), url(r"^people/(?P\d+)/$", views.person, name="person"), url(r"^occupations/(?P\d+)/$", views.occupation, name="occupation"), url(r'^&\'"/(?P\d+)/$', lambda req: None, name="escaping"), ] django-tables2-2.1.1/tests/app/settings.py0000644000175000017500000000116713542204315020011 0ustar josephjosephDATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} INSTALLED_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", "django_tables2", "tests.app", ] ROOT_URLCONF = "tests.app.urls" SECRET_KEY = "this is super secret" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, "OPTIONS": {"context_processors": ["django.template.context_processors.request"]}, } ] TIME_ZONE = "Europe/Amsterdam" SHORT_DATE_FORMAT = "Y-m-d" TIME_FORMAT = "H:i:s" SHORT_DATETIME_FORMAT = "Y-m-d H:i:s" USE_TZ = True django-tables2-2.1.1/tests/app/migrations/0000755000175000017500000000000013542204315017746 5ustar josephjosephdjango-tables2-2.1.1/tests/app/migrations/0001_initial.py0000644000175000017500000001225413542204315022415 0ustar josephjoseph# Generated by Django 2.0 on 2019-09-23 17:47 from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [("contenttypes", "0002_remove_content_type_name")] operations = [ migrations.CreateModel( name="Group", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), ("name", models.CharField(max_length=200)), ], ), migrations.CreateModel( name="Occupation", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), ("name", models.CharField(max_length=200)), ("boolean", models.NullBooleanField()), ( "boolean_with_choices", models.NullBooleanField(choices=[(True, "Yes"), (False, "No")]), ), ], ), migrations.CreateModel( name="Person", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), ("first_name", models.CharField(max_length=200)), ("last_name", models.CharField(max_length=200, verbose_name="surname")), ( "trans_test", models.CharField(blank=True, max_length=200, verbose_name="translation test"), ), ( "trans_test_lazy", models.CharField( blank=True, max_length=200, verbose_name="translation test lazy" ), ), ("safe", models.CharField(blank=True, max_length=200, verbose_name="Safe")), ("website", models.URLField(blank=True, null=True, verbose_name="web site")), ("birthdate", models.DateField(null=True)), ("object_id", models.PositiveIntegerField(blank=True, null=True)), ( "content_type", models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="contenttypes.ContentType", ), ), ("friends", models.ManyToManyField(to="app.Person")), ( "occupation", models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, related_name="people", to="app.Occupation", verbose_name="occupation of the person", ), ), ], options={"verbose_name": "person", "verbose_name_plural": "people"}, ), migrations.CreateModel( name="PersonInformation", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), ( "person", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="info_list", to="app.Person", verbose_name="Information", ), ), ], ), migrations.CreateModel( name="Region", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), ("name", models.CharField(max_length=200)), ( "mayor", models.OneToOneField( null=True, on_delete=django.db.models.deletion.CASCADE, to="app.Person" ), ), ], options={"ordering": ["name"]}, ), migrations.AddField( model_name="occupation", name="region", field=models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, to="app.Region" ), ), migrations.AddField( model_name="group", name="members", field=models.ManyToManyField(to="app.Person") ), migrations.CreateModel( name="PersonProxy", fields=[], options={"ordering": ("last_name",), "proxy": True, "indexes": []}, bases=("app.person",), ), ] django-tables2-2.1.1/tests/app/migrations/__init__.py0000644000175000017500000000000013542204315022045 0ustar josephjosephdjango-tables2-2.1.1/tests/app/__init__.py0000644000175000017500000000000013542204315017671 0ustar josephjosephdjango-tables2-2.1.1/tests/app/locale/0000755000175000017500000000000013542204315017031 5ustar josephjosephdjango-tables2-2.1.1/tests/app/locale/ua/0000755000175000017500000000000013542204315017436 5ustar josephjosephdjango-tables2-2.1.1/tests/app/locale/ua/LC_MESSAGES/0000755000175000017500000000000013542204315021223 5ustar josephjosephdjango-tables2-2.1.1/tests/app/locale/ua/LC_MESSAGES/django.po0000644000175000017500000000040413542204315023023 0ustar josephjosephmsgid "" msgstr "" "Report-Msgid-Bugs-To: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" msgid "translation test" msgstr "тест перекладу" msgid "translation test lazy" msgstr "тест ленивого перекладу" django-tables2-2.1.1/tests/app/locale/ua/LC_MESSAGES/django.mo0000644000175000017500000000044313542204315023023 0ustar josephjoseph4L`arQ,translation testtranslation test lazyReport-Msgid-Bugs-To: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 тест перекладутест ленивого перекладуdjango-tables2-2.1.1/tests/app/templates/0000755000175000017500000000000013542204315017570 5ustar josephjosephdjango-tables2-2.1.1/tests/app/templates/csrf.html0000644000175000017500000000002113542204315021404 0ustar josephjoseph{% csrf_token %} django-tables2-2.1.1/tests/app/templates/multiple.html0000644000175000017500000000021613542204315022310 0ustar josephjoseph{% load django_tables2 %}

Multiple tables using MultiTableMixin

{% for table in tables %} {% render_table table %} {% endfor %} django-tables2-2.1.1/tests/app/templates/minimal.html0000644000175000017500000000100713542204315022102 0ustar josephjoseph{% load django_tables2 %} {% for column in table.columns %} {% endfor %} {% for row in table.paginated_rows %} {% for column, cell in row.items %} {% endfor %} {% endfor %} {% if table.page.has_next %} next {% endif %}
{{ column.header }}
{{ cell }}
django-tables2-2.1.1/tests/app/templates/test_template_column.html0000644000175000017500000000006013542204315024701 0ustar josephjosephname:{{ record.col }}-{{ foo|default:"empty" }} django-tables2-2.1.1/tests/app/templates/dummy.html0000644000175000017500000000003013542204315021602 0ustar josephjosephdummy template contents django-tables2-2.1.1/tests/app/templates/child/0000755000175000017500000000000013542204315020653 5ustar josephjosephdjango-tables2-2.1.1/tests/app/templates/child/foo.html0000644000175000017500000000000413542204315022316 0ustar josephjosephbar django-tables2-2.1.1/tests/app/models.py0000644000175000017500000000555713542204315017443 0ustar josephjosephfrom django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.urls import reverse from django.utils.safestring import mark_safe from django.utils.translation import gettext, gettext_lazy class Person(models.Model): first_name = models.CharField(max_length=200) last_name = models.CharField(max_length=200, verbose_name="surname") occupation = models.ForeignKey( "Occupation", related_name="people", null=True, verbose_name="occupation of the person", on_delete=models.CASCADE, ) trans_test = models.CharField( max_length=200, blank=True, verbose_name=gettext("translation test") ) trans_test_lazy = models.CharField( max_length=200, blank=True, verbose_name=gettext_lazy("translation test lazy") ) safe = models.CharField(max_length=200, blank=True, verbose_name=mark_safe("Safe")) website = models.URLField(max_length=200, null=True, blank=True, verbose_name="web site") birthdate = models.DateField(null=True) content_type = models.ForeignKey(ContentType, null=True, blank=True, on_delete=models.CASCADE) object_id = models.PositiveIntegerField(null=True, blank=True) foreign_key = GenericForeignKey() friends = models.ManyToManyField("Person") class Meta: verbose_name = "person" verbose_name_plural = "people" def __str__(self): return self.first_name @property def name(self): return "%s %s" % (self.first_name, self.last_name) def get_absolute_url(self): return reverse("person", args=(self.pk,)) class PersonProxy(Person): class Meta: proxy = True ordering = ("last_name",) class Group(models.Model): name = models.CharField(max_length=200) members = models.ManyToManyField("Person") def __str__(self): return self.name def get_absolute_url(self): return "/group/{}/".format(self.pk) class Occupation(models.Model): name = models.CharField(max_length=200) region = models.ForeignKey("Region", null=True, on_delete=models.CASCADE) boolean = models.NullBooleanField(null=True) boolean_with_choices = models.NullBooleanField( null=True, choices=((True, "Yes"), (False, "No")) ) def get_absolute_url(self): return reverse("occupation", args=(self.pk,)) def __str__(self): return self.name class Region(models.Model): name = models.CharField(max_length=200) mayor = models.OneToOneField(Person, null=True, on_delete=models.CASCADE) class Meta: ordering = ["name"] def __str__(self): return self.name class PersonInformation(models.Model): person = models.ForeignKey( Person, related_name="info_list", verbose_name="Information", on_delete=models.CASCADE ) django-tables2-2.1.1/tests/test_extra_columns.py0000644000175000017500000001641313542204315021313 0ustar josephjosephfrom django.contrib.auth import get_user_model from django.template import Context, Template from django.test import TestCase from django.utils.translation import gettext_lazy as _ import django_tables2 as tables from .app.models import Person from .utils import build_request, parse User = get_user_model() data = [ {"name": "Adrian", "country": "Australia"}, {"name": "Roy", "country": "Brazil"}, {"name": "Audrey", "country": "Chile"}, {"name": "Bassie", "country": "Belgium"}, ] class DynamicColumnsTest(TestCase): # shim can be dropped when we drop support for python 2.7 and 3.4 if not hasattr(TestCase, "assertRegex"): assertRegex = TestCase.assertRegexpMatches if not hasattr(TestCase, "assertNotRegex"): assertNotRegex = TestCase.assertNotRegexpMatches def test_dynamically_adding_columns(self): """ When adding columns to self.base_columns, they were actually added to the class attribute `Table.base_columns`, and not to the instance attribute, `table.base_columns` issue #403 """ class MyTable(tables.Table): name = tables.Column() # this is obvious: self.assertEqual(list(MyTable(data).columns.columns.keys()), ["name"]) self.assertEqual( list( MyTable(data, extra_columns=[("country", tables.Column())]).columns.columns.keys() ), ["name", "country"], ) # this new instance should not have the extra columns added to the first instance. self.assertEqual(list(MyTable(data).columns.columns.keys()), ["name"]) def test_dynamically_removing_columns(self): class MyTable(tables.Table): name = tables.Column() # this is obvious: self.assertEqual(list(MyTable(data).columns.columns.keys()), ["name"]) self.assertEqual( list( MyTable( data, extra_columns=[("country", tables.Column()), ("name", None)] ).columns.columns.keys() ), ["country"], ) # this new instance should not have the extra columns added to the first instance. self.assertEqual(list(MyTable(data).columns.columns.keys()), ["name"]) def test_sorting_on_dynamically_added_columns(self): class MyTable(tables.Table): name = tables.Column() table = MyTable( data, order_by="-country", extra_columns=[("country", tables.Column(verbose_name=_("country")))], ) root = parse(table.as_html(build_request())) self.assertEqual(root.find(".//tbody/tr/td[2]").text, "Chile") self.assertEqual(root.find(".//tbody/tr[4]/td[2]").text, "Australia") def test_dynamically_override_auto_generated_columns(self): for name, country in data: Person.objects.create(first_name=name, last_name=country) queryset = Person.objects.all() class MyTable(tables.Table): class Meta: model = Person fields = ("first_name", "last_name") self.assertEqual( list(MyTable(queryset).columns.columns.keys()), ["first_name", "last_name"] ) table = MyTable( queryset, extra_columns=[("first_name", tables.Column(attrs={"td": {"style": "color: red;"}}))], ) # we still should have two columns self.assertEqual(list(table.columns.columns.keys()), ["first_name", "last_name"]) # the attrs should be applied to the `first_name` column self.assertEqual( table.columns["first_name"].attrs["td"], {"style": "color: red;", "class": None} ) def test_dynamically_add_column_with_sequence(self): class MyTable(tables.Table): name = tables.Column() class Meta: sequence = ("...", "name") self.assertEqual( list( MyTable(data, extra_columns=[("country", tables.Column())]).columns.columns.keys() ), ["country", "name"], ) # override sequence with an argument. self.assertEqual( list( MyTable( data, extra_columns=[("country", tables.Column())], sequence=("name", "...") ).columns.columns.keys() ), ["name", "country"], ) def test_dynamically_hide_columns(self): class MyTable(tables.Table): name = tables.Column(orderable=False) country = tables.Column(orderable=False) def before_render(self, request): if request.user.username == "Bob": self.columns.hide("country") else: self.columns.show("country") template = Template("{% load django_tables2 %}{% render_table table %}") re_Name = r"\s*Name\s*" re_Country = r"\s*Country\s*" table = MyTable(data) request = build_request(user=User.objects.create(username="Bob")) html = table.as_html(request) self.assertRegex(html, re_Name) self.assertNotRegex(html, re_Country) html = template.render(Context({"request": request, "table": table})) self.assertRegex(html, re_Name) self.assertNotRegex(html, re_Country) request = build_request(user=User.objects.create(username="Alice")) html = table.as_html(request) self.assertRegex(html, re_Name) self.assertRegex(html, re_Country) html = template.render(Context({"request": request, "table": table})) self.assertRegex(html, re_Name) self.assertRegex(html, re_Country) def test_sequence_and_extra_columns(self): """ https://github.com/jieter/django-tables2/issues/486 The exact moment the '...' is expanded is crucial here. """ add_occupation_column = True class MyTable(tables.Table): class Meta: model = Person fields = ("first_name", "friends") sequence = ("first_name", "...", "friends") def __init__(self, data, *args, **kwargs): kwargs["extra_columns"] = kwargs.get("extra_columns", []) if add_occupation_column: kwargs["extra_columns"].append( ("occupation", tables.RelatedLinkColumn(orderable=False)) ) super().__init__(data, *args, **kwargs) table = MyTable(Person.objects.all()) self.assertEqual([c.name for c in table.columns], ["first_name", "occupation", "friends"]) add_occupation_column = False table = MyTable(Person.objects.all()) self.assertEqual([c.name for c in table.columns], ["first_name", "friends"]) def test_change_attributes(self): """ https://github.com/jieter/django-tables2/issues/574 """ class Table(tables.Table): mycolumn = tables.Column(orderable=False) def __init__(self, *args, **kwargs): self.base_columns["mycolumn"].verbose_name = "Monday" super().__init__(*args, **kwargs) table = Table([]) self.assertEqual(table.columns["mycolumn"].verbose_name, "Monday") django-tables2-2.1.1/tests/test_views.py0000644000175000017500000003754413542204315017575 0ustar josephjosephimport django_filters as filters from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from django.views.generic import TemplateView from django_filters.views import FilterView import django_tables2 as tables from .app.models import Person, Region from .utils import build_request MEMORY_DATA = [ {"name": "Queensland"}, {"name": "New South Wales"}, {"name": "Victoria"}, {"name": "Tasmania"}, ] class SimpleTable(tables.Table): class Meta: model = Region exclude = ("id",) class SimpleView(tables.SingleTableView): table_class = SimpleTable model = Region # required for ListView class SingleTableViewTest(TestCase): @classmethod def setUpClass(cls): super().setUpClass() for region in MEMORY_DATA: Region.objects.create(name=region["name"]) def test_should_support_pagination_options(self): class SimplePaginatedView(tables.SingleTableView): table_class = SimpleTable table_pagination = {"per_page": 1} model = Region response = SimplePaginatedView.as_view()(build_request()) table = response.context_data["table"] self.assertEqual(table.paginator.num_pages, len(MEMORY_DATA)) self.assertEqual(table.paginator.per_page, 1) def test_should_support_pagination_options_listView(self): class SimplePaginatedView(tables.SingleTableView): table_class = SimpleTable paginate_by = 1 model = Region response = SimplePaginatedView.as_view()(build_request()) table = response.context_data["table"] self.assertEqual(table.paginator.num_pages, len(MEMORY_DATA)) self.assertEqual(table.paginator.per_page, 1) def test_should_support_default_pagination(self): total_records = 50 for i in range(1, total_records + 1): Region.objects.create(name="region {:02d} / {}".format(i, total_records)) expected_per_page = 25 class PaginateDefault(tables.SingleTableMixin, TemplateView): table_class = SimpleTable template_name = "minimal.html" def get_table_data(self): return Region.objects.all() response = PaginateDefault.as_view()(build_request()) response.render() table = response.context_data["table"] self.assertEqual(table.paginator.per_page, expected_per_page) self.assertEqual(table.paginator.num_pages, 3) self.assertEqual(len(table.page), expected_per_page) # add one for the header row. self.assertEqual(response.content.decode().count(""), expected_per_page + 1) # in addition to New South Wales, Queensland, Tasmania and Victoria, # 21 records should be displayed. self.assertContains(response, "21 / {}".format(total_records)) self.assertNotContains(response, "22 / {}".format(total_records)) def test_should_support_default_pagination_with_table_options(self): class Table(tables.Table): class Meta: model = Region per_page = 2 class PaginateByDefinedOnView(tables.SingleTableView): table_class = Table model = Region table_data = MEMORY_DATA response = PaginateByDefinedOnView.as_view()(build_request()) table = response.context_data["table"] self.assertEqual(table.paginator.per_page, 2) self.assertEqual(len(table.page), 2) def test_should_support_disabling_pagination_options(self): class SimpleNotPaginatedView(tables.SingleTableView): table_class = SimpleTable table_data = MEMORY_DATA table_pagination = False model = Region # required for ListView response = SimpleNotPaginatedView.as_view()(build_request()) table = response.context_data["table"] self.assertFalse(hasattr(table, "page")) def test_data_from_get_queryset(self): class GetQuerysetView(SimpleView): def get_queryset(self): return Region.objects.filter(name__startswith="Q") response = GetQuerysetView.as_view()(build_request()) table = response.context_data["table"] self.assertEqual(len(table.rows), 1) self.assertEqual(table.rows[0].get_cell("name"), "Queensland") def test_should_support_explicit_table_data(self): class ExplicitDataView(tables.SingleTableView): table_class = SimpleTable table_pagination = {"per_page": 1} model = Region table_data = MEMORY_DATA response = ExplicitDataView.as_view()(build_request()) table = response.context_data["table"] self.assertEqual(table.paginator.num_pages, len(MEMORY_DATA)) def test_paginate_by_on_view_class(self): class Table(tables.Table): class Meta: model = Region class PaginateByDefinedOnView(tables.SingleTableView): table_class = Table model = Region paginate_by = 2 table_data = MEMORY_DATA def get_queryset(self): return Region.objects.all().order_by("name") response = PaginateByDefinedOnView.as_view()(build_request()) table = response.context_data["table"] self.assertEqual(table.paginator.per_page, 2) def test_with_custom_paginator(self): # defined in paginator_class class View(tables.SingleTableView): table_class = tables.Table queryset = Region.objects.all() paginator_class = tables.LazyPaginator response = View.as_view()(build_request()) self.assertIsInstance(response.context_data["table"].paginator, tables.LazyPaginator) # defined in table_pagination class View(tables.SingleTableView): table_class = tables.Table queryset = Region.objects.all() table_pagination = {"paginator_class": tables.LazyPaginator} paginate_orphans = 10 response = View.as_view()(build_request()) paginator = response.context_data["table"].paginator self.assertIsInstance(paginator, tables.LazyPaginator) self.assertEqual(paginator.orphans, 10) def test_should_pass_kwargs_to_table_constructor(self): class PassKwargsView(SimpleView): table_data = [] def get_table(self, **kwargs): kwargs.update({"orderable": False}) return super().get_table(**kwargs) response = SimpleView.as_view()(build_request("/")) self.assertTrue(response.context_data["table"].orderable) response = PassKwargsView.as_view()(build_request("/")) self.assertFalse(response.context_data["table"].orderable) def test_should_override_table_pagination(self): class PrefixedTable(SimpleTable): class Meta(SimpleTable.Meta): prefix = "p_" class PrefixedView(SimpleView): table_class = PrefixedTable class PaginationOverrideView(PrefixedView): table_data = MEMORY_DATA def get_table_pagination(self, table): assert isinstance(table, tables.Table) per_page = self.request.GET.get("%s_override" % table.prefixed_per_page_field) if per_page is not None: return {"per_page": per_page} return super().get_table_pagination(table) response = PaginationOverrideView.as_view()(build_request("/?p_per_page_override=2")) self.assertEqual(response.context_data["table"].paginator.per_page, 2) def test_using_get_queryset(self): """ Should not raise a value-error for a View using View.get_queryset() (test for reverting regressing in #423) """ Person.objects.create(first_name="Anton", last_name="Sam") class Table(tables.Table): class Meta: model = Person fields = ("first_name", "last_name") class TestView(tables.SingleTableView): model = Person table_class = Table def get(self, request, *args, **kwargs): self.get_table() from django.http import HttpResponse return HttpResponse() def get_queryset(self): return Person.objects.all() TestView.as_view()(build_request()) def test_get_tables_class(self): view = SimpleView() table_class = view.get_table_class() self.assertEqual(table_class, view.table_class) def test_get_tables_class_auto(self): class SimpleNoTableClassView(tables.SingleTableView): model = Region view = SimpleNoTableClassView() table_class = view.get_table_class() self.assertEqual(table_class.__name__, "RegionAutogeneratedTable") def test_get_tables_class_raises_no_model(self): class SimpleNoTableClassNoModelView(tables.SingleTableView): model = None table_class = None view = SimpleNoTableClassNoModelView() with self.assertRaises(ImproperlyConfigured): view.get_table_class() class SingleTableMixinTest(TestCase): def test_with_non_paginated_view(self): """ SingleTableMixin should not assume it is mixed with a ListView Github issue #326 """ class Table(tables.Table): class Meta: model = Region class View(tables.SingleTableMixin, TemplateView): table_class = Table table_data = MEMORY_DATA template_name = "dummy.html" View.as_view()(build_request()) def test_should_paginate_by_default(self): """ When mixing SingleTableMixin with FilterView, the table should paginate by default """ total_records = 60 for i in range(1, total_records + 1): Region.objects.create(name="region {i:02d} / {total_records}".format(**locals())) expected_per_page = 25 class RegionFilter(filters.FilterSet): name = filters.CharFilter(lookup_expr="icontains") class PaginateDefault(tables.SingleTableMixin, FilterView): table_class = SimpleTable model = Region filterset_class = RegionFilter template_name = "minimal.html" response = PaginateDefault.as_view()(build_request()) response.render() table = response.context_data["table"] self.assertEqual(table.paginator.per_page, expected_per_page) self.assertEqual(table.paginator.num_pages, 3) self.assertEqual(len(table.page), expected_per_page) self.assertEqual(response.content.decode().count(""), expected_per_page + 1) class TableA(tables.Table): class Meta: model = Person class TableB(tables.Table): class Meta: model = Region exclude = ("id",) class MultiTableMixinTest(TestCase): @classmethod def setUpClass(cls): super().setUpClass() Person.objects.create(first_name="Jan Pieter", last_name="W") NL_PROVICES = ( "Flevoland", "Friesland", "Gelderland", "Groningen", "Limburg", "Noord-Brabant", "Noord-Holland", "Overijssel", "Utrecht", "Zeeland", "Zuid-Holland", ) for name in NL_PROVICES: Region.objects.create(name=name) def test_basic(self): class View(tables.MultiTableMixin, TemplateView): tables = (TableA, TableB) tables_data = (Person.objects.all(), Region.objects.all()) template_name = "multiple.html" response = View.as_view()(build_request("/")) response.render() html = response.rendered_content self.assertIn("table_0-sort=first_name", html) self.assertIn("table_1-sort=name", html) self.assertIn("Jan Pieter", html) self.assertIn("Zuid-Holland", html) def test_supplying_instances(self): class View(tables.MultiTableMixin, TemplateView): tables = (TableA(Person.objects.all()), TableB(Region.objects.all())) template_name = "multiple.html" response = View.as_view()(build_request("/")) response.render() html = response.rendered_content self.assertIn("table_0-sort=first_name", html) self.assertIn("table_1-sort=name", html) self.assertIn("Jan Pieter", html) self.assertIn("Zuid-Holland", html) def test_without_tables(self): class View(tables.MultiTableMixin, TemplateView): template_name = "multiple.html" with self.assertRaises(ImproperlyConfigured): View.as_view()(build_request("/")) def test_with_empty_get_tables_list(self): class View(tables.MultiTableMixin, TemplateView): template_name = "multiple.html" def get_tables(self): return [] response = View.as_view()(build_request("/")) response.render() html = response.rendered_content self.assertIn("

Multiple tables using MultiTableMixin

", html) def test_with_empty_class_tables_list(self): class View(tables.MultiTableMixin, TemplateView): template_name = "multiple.html" tables = [] response = View.as_view()(build_request("/")) response.render() html = response.rendered_content self.assertIn("

Multiple tables using MultiTableMixin

", html) def test_length_mismatch(self): class View(tables.MultiTableMixin, TemplateView): tables = (TableA, TableB) tables_data = (Person.objects.all(),) template_name = "multiple.html" with self.assertRaises(ImproperlyConfigured): View.as_view()(build_request("/")) def test_table_pagination(self): class View(tables.MultiTableMixin, TemplateView): tables = (TableB(Region.objects.all()), TableB(Region.objects.all())) template_name = "multiple.html" table_pagination = {"per_page": 5} response = View.as_view()(build_request("/?table_1-page=3")) tableA, tableB = response.context_data["tables"] self.assertEqual(tableA.page.number, 1) self.assertEqual(tableB.page.number, 3) def test_paginate_by(self): class View(tables.MultiTableMixin, TemplateView): tables = (TableB(Region.objects.all()), TableB(Region.objects.all())) template_name = "multiple.html" paginate_by = 5 response = View.as_view()(build_request("/?table_1-page=3")) tableA, tableB = response.context_data["tables"] self.assertEqual(tableA.page.number, 1) self.assertEqual(tableB.page.number, 3) def test_get_tables_data(self): class View(tables.MultiTableMixin, TemplateView): tables = (TableA, TableB) template_name = "multiple.html" def get_tables_data(self): return [Person.objects.all(), Region.objects.all()] response = View.as_view()(build_request("/")) response.render() html = response.rendered_content self.assertIn("Jan Pieter", html) self.assertIn("Zuid-Holland", html) def test_table_prefix(self): class View(tables.MultiTableMixin, TemplateView): tables = ( TableB(Region.objects.all(), prefix="test_prefix"), TableB(Region.objects.all()), ) template_name = "multiple.html" response = View.as_view()(build_request("/")) tableA = response.context_data["tables"][0] tableB = response.context_data["tables"][1] self.assertEqual("test_prefix", tableA.prefix) self.assertIn("table", tableB.prefix) django-tables2-2.1.1/tests/__init__.py0000644000175000017500000000000013542204315017111 0ustar josephjosephdjango-tables2-2.1.1/tests/columns/0000755000175000017500000000000013542204315016472 5ustar josephjosephdjango-tables2-2.1.1/tests/columns/test_datecolumn.py0000644000175000017500000000566413542204315022251 0ustar josephjosephfrom datetime import date from django.db import models from django.test import SimpleTestCase, override_settings import django_tables2 as tables def isoformat_link(value): return "/test/{}/".format(value.isoformat()) class DateColumnTest(SimpleTestCase): """ Format string: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date D -- Day of the week, textual, 3 letters -- 'Fri' b -- Month, textual, 3 letters, lowercase -- 'jan' Y -- Year, 4 digits. -- '1999' """ def test_should_handle_explicit_format(self): class TestTable(tables.Table): date = tables.DateColumn(format="D b Y") date_linkify = tables.DateColumn( accessor="date", format="D b Y", linkify=isoformat_link ) class Meta: default = "—" table = TestTable([{"date": date(2012, 9, 11)}, {"date": None}]) self.assertEqual(table.rows[0].get_cell("date"), "Tue sep 2012") self.assertEqual( table.rows[0].get_cell("date_linkify"), 'Tue sep 2012' ) self.assertEqual(table.rows[1].get_cell("date"), "—") @override_settings(DATE_FORMAT="D Y b") def test_should_handle_long_format(self): class TestTable(tables.Table): date = tables.DateColumn(short=False) class Meta: default = "—" table = TestTable([{"date": date(2012, 9, 11)}, {"date": None}]) assert table.rows[0].get_cell("date") == "Tue 2012 sep" assert table.rows[1].get_cell("date") == "—" @override_settings(SHORT_DATE_FORMAT="b Y D") def test_should_handle_short_format(self): class TestTable(tables.Table): date = tables.DateColumn(short=True) class Meta: default = "—" table = TestTable([{"date": date(2012, 9, 11)}, {"date": None}]) assert table.rows[0].get_cell("date") == "sep 2012 Tue" assert table.rows[1].get_cell("date") == "—" def test_should_be_used_for_datefields(self): class DateModel(models.Model): field = models.DateField() class Meta: app_label = "django_tables2_test" class Table(tables.Table): class Meta: model = DateModel self.assertEqual(type(Table.base_columns["field"]), tables.DateColumn) @override_settings(SHORT_DATE_FORMAT="b Y D") def test_value_returns_a_raw_value_without_html(self): class Table(tables.Table): date = tables.DateColumn() date_linkify = tables.DateColumn(accessor="date", linkify=isoformat_link) table = Table([{"date": date(2012, 9, 12)}]) self.assertEqual(table.rows[0].get_cell_value("date"), "sep 2012 Wed") self.assertEqual( table.rows[0].get_cell("date_linkify"), 'sep 2012 Wed' ) django-tables2-2.1.1/tests/columns/test_checkboxcolumn.py0000644000175000017500000000631313542204315023112 0ustar josephjosephfrom django.test import SimpleTestCase import django_tables2 as tables from ..utils import attrs class CheckBoxColumnTest(SimpleTestCase): def test_new_attrs_should_be_supported(self): class TestTable(tables.Table): col1 = tables.CheckBoxColumn( attrs=dict(th__input={"th_key": "th_value"}, td__input={"td_key": "td_value"}) ) col2 = tables.CheckBoxColumn(attrs=dict(input={"key": "value"})) table = TestTable([{"col1": "data", "col2": "data"}]) assert attrs(table.columns["col1"].header) == {"type": "checkbox", "th_key": "th_value"} assert attrs(table.rows[0].get_cell("col1")) == { "type": "checkbox", "td_key": "td_value", "value": "data", "name": "col1", } assert attrs(table.columns["col2"].header) == {"type": "checkbox", "key": "value"} assert attrs(table.rows[0].get_cell("col2")) == { "type": "checkbox", "key": "value", "value": "data", "name": "col2", } def test_column_is_checked(self): class TestTable(tables.Table): col = tables.CheckBoxColumn(attrs={"name": "col"}, checked="is_selected") table = TestTable([{"col": "1", "is_selected": True}, {"col": "2", "is_selected": False}]) assert attrs(table.rows[0].get_cell("col")) == { "type": "checkbox", "value": "1", "name": "col", "checked": "checked", } assert attrs(table.rows[1].get_cell("col")) == { "type": "checkbox", "value": "2", "name": "col", } def test_column_is_not_checked_for_non_existing_column(self): class TestTable(tables.Table): col = tables.CheckBoxColumn(checked="does_not_exist") table = TestTable([{"col": "1", "is_selected": True}, {"col": "2", "is_selected": False}]) assert attrs(table.rows[0].get_cell("col")) == { "type": "checkbox", "value": "1", "name": "col", } assert attrs(table.rows[1].get_cell("col")) == { "type": "checkbox", "value": "2", "name": "col", } def test_column_is_alway_checked(self): class TestTable(tables.Table): col = tables.CheckBoxColumn(checked=True) table = TestTable([{"col": 1, "foo": "bar"}, {"col": 2, "foo": "baz"}]) assert attrs(table.rows[0].get_cell("col"))["checked"] == "checked" assert attrs(table.rows[1].get_cell("col"))["checked"] == "checked" def test_column_is_checked_callback(self): def is_selected(value, record): return value == "1" class TestTable(tables.Table): col = tables.CheckBoxColumn(attrs={"name": "col"}, checked=is_selected) table = TestTable([{"col": "1"}, {"col": "2"}]) assert attrs(table.rows[0].get_cell("col")) == { "type": "checkbox", "value": "1", "name": "col", "checked": "checked", } assert attrs(table.rows[1].get_cell("col")) == { "type": "checkbox", "value": "2", "name": "col", } django-tables2-2.1.1/tests/columns/test_urlcolumn.py0000644000175000017500000000400313542204315022120 0ustar josephjosephfrom django.db import models from django.test import SimpleTestCase import django_tables2 as tables MEMORY_DATA = [ {"url": "http://example.com", "name": "Example"}, {"url": "https://example.com", "name": "Example (https)"}, {"url": "ftp://example.com", "name": "Example (ftp)"}, ] class UrlColumnTest(SimpleTestCase): def test_should_turn_url_into_hyperlink(self): class TestTable(tables.Table): url = tables.URLColumn() table = TestTable(MEMORY_DATA) self.assertEqual( table.rows[0].get_cell("url"), 'http://example.com' ) self.assertEqual( table.rows[1].get_cell("url"), 'https://example.com' ) def test_should_be_used_for_urlfields(self): class URLModel(models.Model): field = models.URLField() class Meta: app_label = "django_tables2_test" class Table(tables.Table): class Meta: model = URLModel assert type(Table.base_columns["field"]) == tables.URLColumn def test_text_can_be_overridden(self): class Table(tables.Table): url = tables.URLColumn(text="link") table = Table(MEMORY_DATA) assert table.rows[0].get_cell("url") == 'link' def test_text_can_be_overridden_with_callable(self): class Table(tables.Table): url = tables.URLColumn(text=lambda record: record["name"]) table = Table(MEMORY_DATA) assert table.rows[0].get_cell("url") == 'Example' assert table.rows[1].get_cell("url") == 'Example (https)' def test_value_returns_a_raw_value_without_html(self): class TestTable(tables.Table): col = tables.URLColumn() table = TestTable([{"col": "http://example.com"}]) assert table.rows[0].get_cell_value("col") == "http://example.com" django-tables2-2.1.1/tests/columns/test_emailcolumn.py0000644000175000017500000000317413542204315022415 0ustar josephjosephfrom django.db import models from django.test import SimpleTestCase import django_tables2 as tables class EmailColumnTest(SimpleTestCase): def test_should_turn_email_address_into_hyperlink(self): class Table(tables.Table): email = tables.EmailColumn() table = Table([{"email": "test@example.com"}]) self.assertEqual( table.rows[0].get_cell("email"), 'test@example.com', ) def test_should_render_default_for_blank(self): class Table(tables.Table): email = tables.EmailColumn(default="---") table = Table([{"email": ""}]) self.assertEqual(table.rows[0].get_cell("email"), "---") def test_should_be_used_for_emailfields(self): class EmailModel(models.Model): field = models.EmailField() class Meta: app_label = "test" class Table(tables.Table): class Meta: model = EmailModel self.assertEqual(type(Table.base_columns["field"]), tables.EmailColumn) def test_text_should_be_overridable(self): class Table(tables.Table): email = tables.EmailColumn(text="@") table = Table([{"email": "test@example.com"}]) self.assertEqual(table.rows[0].get_cell("email"), '@') def test_value_returns_a_raw_value_without_html(self): class Table(tables.Table): col = tables.EmailColumn() table = Table([{"col": "test@example.com"}]) self.assertEqual(table.rows[0].get_cell_value("col"), "test@example.com") django-tables2-2.1.1/tests/columns/test_linkcolumn.py0000644000175000017500000002136213542204315022262 0ustar josephjosephfrom django.template import Context, Template from django.test import TestCase from django.urls import reverse from django.utils.html import mark_safe import django_tables2 as tables from django_tables2 import A from ..app.models import Occupation, Person from ..utils import attrs, build_request class LinkColumnTest(TestCase): def test_unicode(self): """Test LinkColumn for unicode values + headings""" class UnicodeTable(tables.Table): first_name = tables.LinkColumn("person", args=[A("pk")]) last_name = tables.LinkColumn( "person", args=[A("pk")], verbose_name="äÚ¨´ˆÁ˜¨ˆ˜˘Ú…Ò˚ˆπ∆ˆ´" ) dataset = [ {"pk": 1, "first_name": "Brädley", "last_name": "∆yers"}, {"pk": 2, "first_name": "Chr…s", "last_name": "DÒble"}, ] template = Template("{% load django_tables2 %}{% render_table table %}") html = template.render( Context({"request": build_request(), "table": UnicodeTable(dataset)}) ) self.assertIn("Brädley", html) self.assertIn("∆yers", html) self.assertIn("Chr…s", html) self.assertIn("DÒble", html) def test_link_text_custom_value(self): class CustomLinkTable(tables.Table): first_name = tables.LinkColumn("person", text="foo::bar", args=[A("pk")]) last_name = tables.LinkColumn( "person", text=lambda row: "%s %s" % (row["last_name"], row["first_name"]), args=[A("pk")], ) dataset = [{"pk": 1, "first_name": "John", "last_name": "Doe"}] html = CustomLinkTable(dataset).as_html(build_request()) self.assertIn("foo::bar", html) self.assertIn("Doe John", html) def test_link_text_escaping(self): class CustomLinkTable(tables.Table): editlink = tables.LinkColumn("person", text=mark_safe("edit"), args=[A("pk")]) dataset = [{"pk": 1, "first_name": "John", "last_name": "Doe"}] html = CustomLinkTable(dataset).as_html(build_request()) expected = 'edit'.format(reverse("person", args=(1,))) self.assertIn(expected, html) def test_null_foreign_key(self): class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.Column() occupation = tables.LinkColumn("occupation", args=[A("occupation.pk")]) occupation_linkify = tables.Column(linkify=("occupation", A("occupation.pk"))) Person.objects.create(first_name="bradley", last_name="ayers") table = PersonTable(Person.objects.all()) html = table.as_html(build_request()) self.assertIn("—", html) def test_linkcolumn_non_field_based(self): """Test for issue 257, non-field based columns""" class Table(tables.Table): first_name = tables.Column() delete_link = tables.LinkColumn( "person_delete", text="delete", kwargs={"pk": tables.A("id")} ) willem = Person.objects.create(first_name="Willem", last_name="Wever") table = Table(Person.objects.all()) expected = 'delete'.format( reverse("person_delete", kwargs={"pk": willem.pk}) ) self.assertEqual(table.rows[0].get_cell("delete_link"), expected) def test_kwargs(self): class PersonTable(tables.Table): a = tables.LinkColumn("occupation", kwargs={"pk": A("a")}) table = PersonTable([{"a": 0}, {"a": 1}]) self.assertIn(reverse("occupation", kwargs={"pk": 0}), table.rows[0].get_cell("a")) self.assertIn(reverse("occupation", kwargs={"pk": 1}), table.rows[1].get_cell("a")) def test_html_escape_value(self): class PersonTable(tables.Table): name = tables.LinkColumn("escaping", kwargs={"pk": A("pk")}) name_linkify = tables.Column(accessor="name", linkify=("escaping", {"pk": A("pk")})) table = PersonTable([{"name": "", "pk": 1}]) # django==3.0 replaces ' with ', drop first option if django==2.2 support is removed self.assertIn( table.rows[0].get_cell("name"), ( '<brad>' '<brad>' ), ) # the two columns should result in the same rendered cell contents self.assertEqual(table.rows[0].get_cell("name"), table.rows[0].get_cell("name_linkify")) def test_a_attrs_should_be_supported(self): class TestTable(tables.Table): col = tables.LinkColumn( "occupation", kwargs={"pk": A("col")}, attrs={"a": {"title": "Occupation Title"}} ) col_linkify = tables.Column( accessor="col", attrs={"a": {"title": "Occupation Title"}}, linkify=("occupation", {"pk": A("col")}), ) table = TestTable([{"col": 0}]) self.assertEqual( attrs(table.rows[0].get_cell("col")), {"href": reverse("occupation", kwargs={"pk": 0}), "title": "Occupation Title"}, ) self.assertEqual(table.rows[0].get_cell("col"), table.rows[0].get_cell("col_linkify")) def test_td_attrs_should_be_supported(self): """LinkColumn should support both and attrs""" person = Person.objects.create(first_name="Bob", last_name="Builder") class Table(tables.Table): first_name = tables.LinkColumn( attrs={"a": {"style": "color: red;"}, "td": {"style": "background-color: #ddd;"}} ) last_name = tables.Column() table = Table(Person.objects.all()) a_tag = table.rows[0].get_cell("first_name") self.assertIn('href="{}"'.format(reverse("person", args=(person.pk,))), a_tag) self.assertIn('style="color: red;"', a_tag) self.assertIn(person.first_name, a_tag) html = table.as_html(build_request()) self.assertIn('', html) def test_defaults(self): class Table(tables.Table): link = tables.LinkColumn("occupation", kwargs={"pk": 1}, default="xyz") table = Table([{}]) self.assertEqual(table.rows[0].get_cell("link"), "xyz") def test_get_absolute_url(self): class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.LinkColumn() other_last_name = tables.Column(accessor="last_name", linkify=True) person = Person.objects.create(first_name="Jan Pieter", last_name="Waagmeester") table = PersonTable(Person.objects.all()) expected = '{}'.format(person.get_absolute_url(), person.last_name) self.assertEqual(table.rows[0].cells["last_name"], expected) # Explicit LinkColumn and regular column using linkify should have equal output self.assertEqual( table.rows[0].get_cell("other_last_name"), table.rows[0].get_cell("last_name") ) def test_get_absolute_url_not_defined(self): """A dict doesn't have a get_absolute_url(), so creating the table should raise a TypeError.""" class Table(tables.Table): first_name = tables.Column() last_name = tables.LinkColumn() table = Table([dict(first_name="Jan Pieter", last_name="Waagmeester")]) with self.assertRaises(TypeError): table.as_html(build_request()) def test_RelatedLinkColumn(self): carpenter = Occupation.objects.create(name="Carpenter") Person.objects.create(first_name="Bob", last_name="Builder", occupation=carpenter) class Table(tables.Table): occupation = tables.RelatedLinkColumn() occupation_linkify = tables.Column(accessor="occupation", linkify=True) table = Table(Person.objects.all()) self.assertEqual( table.rows[0].cells["occupation"], 'Carpenter'.format(reverse("occupation", args=[carpenter.pk])), ) def test_RelatedLinkColumn_without_model(self): class Table(tables.Table): occupation = tables.RelatedLinkColumn() table = Table([{"occupation": "Fabricator"}]) msg = "for linkify=True, 'Fabricator' must have a method get_absolute_url" with self.assertRaisesMessage(TypeError, msg): table.rows[0].cells["occupation"] def test_value_returns_a_raw_value_without_html(self): class Table(tables.Table): col = tables.LinkColumn("occupation", args=(A("id"),)) table = Table([{"col": "link-text", "id": 1}]) self.assertEqual(table.rows[0].get_cell_value("col"), "link-text") django-tables2-2.1.1/tests/columns/test_jsoncolumn.py0000644000175000017500000000330513542204315022273 0ustar josephjosephfrom django.contrib.postgres.fields import HStoreField, JSONField from django.db import models from django.test import SimpleTestCase import django_tables2 as tables class JsonColumnTestCase(SimpleTestCase): def test_should_be_used_for_json_and_hstore_fields(self): class Model(models.Model): json = JSONField() hstore = HStoreField() class Meta: app_label = "django_tables2_test" class Table(tables.Table): class Meta: model = Model self.assertIsInstance(Table.base_columns["json"], tables.JSONColumn) self.assertIsInstance(Table.base_columns["hstore"], tables.JSONColumn) def test_jsoncolumn_attrs(self): column = tables.JSONColumn(attrs={"pre": {"class": "json"}}) record = {"json": "foo"} html = column.render(value=record["json"], record=record) self.assertEqual(html, '
"foo"
') def test_jsoncolumn_dict(self): column = tables.JSONColumn() record = {"json": {"species": "Falcon"}} html = column.render(value=record["json"], record=record) self.assertEqual(html, "
{\n  "species": "Falcon"\n}
") def test_jsoncolumn_string(self): column = tables.JSONColumn() record = {"json": "really?"} html = column.render(value=record["json"], record=record) self.assertEqual(html, "
"really?"
") def test_jsoncolumn_number(self): column = tables.JSONColumn() record = {"json": 3.14} html = column.render(value=record["json"], record=record) self.assertEqual(html, "
3.14
") django-tables2-2.1.1/tests/columns/__init__.py0000644000175000017500000000000013542204315020571 0ustar josephjosephdjango-tables2-2.1.1/tests/columns/test_booleancolumn.py0000644000175000017500000001343413542204315022745 0ustar josephjosephfrom unittest import skipIf from django import VERSION as django_version from django.db import models from django.test import TestCase import django_tables2 as tables from ..app.models import Occupation, Person from ..utils import attrs, build_request, parse class BooleanColumnTest(TestCase): def test_should_be_used_for_booleanfield(self): class BoolModel(models.Model): field = models.BooleanField() class Meta: app_label = "django_tables2_test" class Table(tables.Table): class Meta: model = BoolModel column = Table.base_columns["field"] self.assertEqual(type(column), tables.BooleanColumn) self.assertEqual(column.empty_values, ()) @skipIf(django_version < (2, 1, 0), "Feature added in django 2.1") def test_should_use_nullability_for_booloanfield(self): """ Django 2.1 supports null=(True|False) for BooleanField. """ class BoolModel2(models.Model): field = models.BooleanField(null=True) class Meta: app_label = "django_tables2_test" class Table(tables.Table): class Meta: model = BoolModel2 column = Table.base_columns["field"] self.assertEqual(type(column), tables.BooleanColumn) self.assertEqual(column.empty_values, (None, "")) def test_should_be_used_for_nullbooleanfield(self): class NullBoolModel(models.Model): field = models.NullBooleanField() class Meta: app_label = "django_tables2_test" class Table(tables.Table): class Meta: model = NullBoolModel column = Table.base_columns["field"] self.assertEqual(type(column), tables.BooleanColumn) self.assertNotEqual(column.empty_values, ()) def test_treat_none_different_from_false(self): class Table(tables.Table): col = tables.BooleanColumn(null=True, default="---") table = Table([{"col": None}]) self.assertEqual(table.rows[0].get_cell("col"), "---") def test_treat_none_as_false(self): class Table(tables.Table): col = tables.BooleanColumn(null=False) table = Table([{"col": None}]) self.assertEqual(table.rows[0].get_cell("col"), '') def test_value_returns_a_raw_value_without_html(self): class Table(tables.Table): col = tables.BooleanColumn() table = Table([{"col": True}, {"col": False}]) self.assertEqual(table.rows[0].get_cell_value("col"), "True") self.assertEqual(table.rows[1].get_cell_value("col"), "False") def test_span_attrs(self): class Table(tables.Table): col = tables.BooleanColumn(attrs={"span": {"key": "value"}}) col_linkify = tables.BooleanColumn( accessor="col", attrs={"span": {"key": "value"}}, linkify=lambda value: "/bool/{}".format(value), ) table = Table([{"col": True}, {"col": False}]) self.assertEqual(attrs(table.rows[0].get_cell("col")), {"class": "true", "key": "value"}) self.assertEqual(attrs(table.rows[1].get_cell("col")), {"class": "false", "key": "value"}) self.assertIn(table.rows[0].get_cell("col"), table.rows[0].get_cell("col_linkify")) def test_boolean_field_choices_with_real_model_instances(self): """ If a booleanField has choices defined, the value argument passed to BooleanColumn.render() is the rendered value, not a bool. """ class BoolModelChoices(models.Model): field = models.BooleanField(choices=((True, "Yes"), (False, "No"))) class Meta: app_label = "django_tables2_test" class Table(tables.Table): class Meta: model = BoolModelChoices table = Table([BoolModelChoices(field=True), BoolModelChoices(field=False)]) self.assertEqual(table.rows[0].get_cell("field"), '') self.assertEqual(table.rows[1].get_cell("field"), '') def test_boolean_field_choices_spanning_relations(self): "The inverse lookup voor boolean choices should also work on related models" class Table(tables.Table): boolean = tables.BooleanColumn(accessor="occupation.boolean_with_choices") class Meta: model = Person model_true = Occupation.objects.create(name="true-name", boolean_with_choices=True) model_false = Occupation.objects.create(name="false-name", boolean_with_choices=False) table = Table( [ Person(first_name="True", last_name="False", occupation=model_true), Person(first_name="True", last_name="False", occupation=model_false), ] ) self.assertEqual(table.rows[0].get_cell("boolean"), '') self.assertEqual(table.rows[1].get_cell("boolean"), '') def test_boolean_should_not_prevent_rendering_of_other_columns(self): """Test for issue 360""" class Table(tables.Table): boolean = tables.BooleanColumn(yesno="waar,onwaar") class Meta: model = Occupation fields = ("boolean", "name") Occupation.objects.create(name="Waar", boolean=True), Occupation.objects.create(name="Onwaar", boolean=False), Occupation.objects.create(name="Onduidelijk") html = Table(Occupation.objects.all()).as_html(build_request()) root = parse(html) self.assertEqual(root.findall(".//tbody/tr[1]/td")[1].text, "Waar") self.assertEqual(root.findall(".//tbody/tr[2]/td")[1].text, "Onwaar") django-tables2-2.1.1/tests/columns/test_manytomanycolumn.py0000644000175000017500000001566713542204315023534 0ustar josephjosephfrom random import randint, sample from django.test import TestCase from django.utils.html import format_html, mark_safe, strip_tags import django_tables2 as tables from tests.app.models import Group, Occupation, Person class ManyToManyColumnTest(TestCase): FAKE_NAMES = ( ("Kyle", "Strader"), ("Francis", "Fisher"), ("James", "Jury"), ("Florentina", "Floyd"), ("Mark", "Boyd"), ("Simone", "Fong"), ) def setUp(self): self.carpenter = Occupation.objects.create(name="Carpenter") for first, last in self.FAKE_NAMES: Person.objects.create(first_name=first, last_name=last, occupation=self.carpenter) persons = list(Person.objects.all()) # give everyone 1 to 3 friends for person in persons: person.friends.add(*sample(persons, randint(1, 3))) person.save() # add a person without friends self.remi = Person.objects.create(first_name="Remi", last_name="Barberin") self.developers = Group.objects.create(name="developers") self.developers.members.add( Person.objects.get(first_name="James"), Person.objects.get(first_name="Simone") ) def test_ManyToManyColumn_from_model(self): """ Automatically uses the ManyToManyColumn for a ManyToManyField, and calls the Models's `__str__` method to transform the model instace to string. """ class Table(tables.Table): name = tables.Column(accessor="name", order_by=("last_name", "first_name")) class Meta: model = Person fields = ("name", "friends") table = Table(Person.objects.all()) for row in table.rows: cell = row.get_cell("friends") if cell is None: continue for friend in cell.split(", "): self.assertTrue(Person.objects.filter(first_name=friend).exists()) def test_ManyToManyColumn_linkify_item(self): class Table(tables.Table): name = tables.Column(accessor="name", order_by=("last_name", "first_name")) friends = tables.ManyToManyColumn(linkify_item=True) table = Table(Person.objects.all()) for row in table.rows: friends = row.get_cell("friends") for friend in row.record.friends.all(): self.assertIn(friend.get_absolute_url(), friends) self.assertIn(str(friend), friends) def test_ManyToManyColumn_linkify_item_different_model(self): """ Make sure the correct get_absolute_url() is used to linkify the items. """ class GroupTable(tables.Table): name = tables.Column(linkify=True) members = tables.ManyToManyColumn(linkify_item=True) row = GroupTable(Group.objects.all()).rows[0] self.assertEqual( row.get_cell("name"), '{}'.format(self.developers.pk, self.developers.name), ) self.assertEqual( row.get_cell("members"), 'James, Simone', ) def test_ManyToManyColumn_linkify_item_foreign_key(self): class OccupationTable(tables.Table): name = tables.Column(linkify=True) people = tables.ManyToManyColumn(linkify_item=True) row = OccupationTable(Occupation.objects.all()).rows[0] self.assertEqual( row.get_cell("name"), '{}'.format(self.carpenter.pk, self.carpenter.name), ) self.assertEqual( row.get_cell("people"), ", ".join( ( 'Kyle', 'Francis', 'James', 'Florentina', 'Mark', 'Simone', ) ), ) def test_custom_separator(self): def assert_sep(sep): class Table(tables.Table): friends = tables.ManyToManyColumn(separator=sep) table = Table(Person.objects.all().order_by("last_name")) for row in table.rows: cell = row.get_cell("friends") if cell is None: continue for friend in cell.split(sep): self.assertTrue(Person.objects.filter(first_name=friend).exists()) # normal string, will not be escaped assert_sep("|") # html tag, would normally be escaped, but should not be escaped because # it is mark_safe()'ed assert_sep(mark_safe("
")) def test_transform_returns_html(self): class Table(tables.Table): friends = tables.ManyToManyColumn( transform=lambda m: format_html("{}", m.first_name) ) table = Table(Person.objects.all().order_by("last_name")) for row in table.rows: cell = row.get_cell("friends") if cell is None: continue for friend in cell.split(", "): stripped = strip_tags(friend) self.assertTrue(Person.objects.filter(first_name=stripped).exists()) def test_orderable_is_false(self): class Table(tables.Table): friends = tables.ManyToManyColumn(orderable=False) table = Table([]) self.assertFalse(table.columns["friends"].orderable) def test_ManyToManyColumn_complete_example(self): class Table(tables.Table): name = tables.Column(accessor="name", order_by=("last_name", "first_name")) friends = tables.ManyToManyColumn( transform=lambda o: o.name, filter=lambda o: o.order_by("-last_name") ) table = Table(Person.objects.all().order_by("last_name")) for row in table.rows: friends = row.get_cell("friends") if friends is None: self.assertEqual(row.get_cell("name"), self.remi.name) continue # verify the list is sorted descending friends = list(map(lambda o: o.split(" "), friends.split(", "))) self.assertEqual(friends, sorted(friends, key=lambda item: item[1], reverse=True)) def test_ManyToManyColumn_custom_default(self): class Table(tables.Table): name = tables.Column(accessor="name", order_by=("last_name", "first_name")) friends = tables.ManyToManyColumn(default="--") table = Table(Person.objects.all().order_by("last_name")) cell_value_with_default = None for row in table.rows: if row.get_cell("name") == self.remi.name: cell_value_with_default = row.get_cell("friends") break self.assertEqual(cell_value_with_default, "--") django-tables2-2.1.1/tests/columns/test_initialsortcolumn.py0000644000175000017500000000437213542204315023670 0ustar josephjosephfrom django.db import models from django.test import TestCase import django_tables2 as tables class InitialSortColumnTest(TestCase): def test_initial_sort_descending_affects_order_by_alias_next(self): class IntModel(models.Model): field = models.IntegerField() class Meta: app_label = "django_tables2_test" class Table(tables.Table): class Meta: model = IntModel class TableDescOrd(tables.Table): field = tables.Column(initial_sort_descending=True) class Meta: model = IntModel data = [{"field": 1}, {"field": 5}, {"field": 3}] # no initial ordering table = Table(data) table_desc = TableDescOrd(data) self.assertEqual(table.columns[1].order_by_alias.next, "field") self.assertEqual(table_desc.columns[1].order_by_alias.next, "-field") # with ascending ordering table = Table(data, order_by=("field",)) table_desc = TableDescOrd(data, order_by=("field",)) self.assertEqual(table.columns[1].order_by_alias.next, "-field") self.assertEqual(table_desc.columns[1].order_by_alias.next, "-field") self.assertEqual(table.rows[0].get_cell("field"), 1) self.assertEqual(table.rows[1].get_cell("field"), 3) self.assertEqual(table.rows[2].get_cell("field"), 5) self.assertEqual(table_desc.rows[0].get_cell("field"), 1) self.assertEqual(table_desc.rows[1].get_cell("field"), 3) self.assertEqual(table_desc.rows[2].get_cell("field"), 5) # with initial descending ordering table = Table(data, order_by=("-field",)) table_desc = TableDescOrd(data, order_by=("-field",)) self.assertEqual(table.columns[1].order_by_alias.next, "field") self.assertEqual(table_desc.columns[1].order_by_alias.next, "field") self.assertEqual(table.rows[0].get_cell("field"), 5) self.assertEqual(table.rows[1].get_cell("field"), 3) self.assertEqual(table.rows[2].get_cell("field"), 1) self.assertEqual(table_desc.rows[0].get_cell("field"), 5) self.assertEqual(table_desc.rows[1].get_cell("field"), 3) self.assertEqual(table_desc.rows[2].get_cell("field"), 1) django-tables2-2.1.1/tests/columns/test_templatecolumn.py0000644000175000017500000001026113542204315023134 0ustar josephjosephfrom django.template import Context, Template from django.test import SimpleTestCase import django_tables2 as tables from ..utils import build_request class TemplateColumnTest(SimpleTestCase): def test_should_render_in_pinned_row(self): class TestOnlyPinnedTable(tables.Table): foo = tables.TemplateColumn("value={{ value }}") def __init__(self, data): self.pinned = data revised_data = [] super(TestOnlyPinnedTable, self).__init__(revised_data) def get_top_pinned_data(self): return self.pinned table = TestOnlyPinnedTable([{"foo": "bar"}]) for row in table.rows: self.assertEqual(row.get_cell("foo"), "value=bar") template = Template("{% load django_tables2 %}{% render_table table %}") html = template.render(Context({"request": build_request(), "table": table})) self.assertIn("value=bar", html) def test_should_handle_context_on_table(self): class TestTable(tables.Table): col_code = tables.TemplateColumn(template_code="code:{{ record.col }}-{{ foo }}") col_name = tables.TemplateColumn(template_name="test_template_column.html") col_context = tables.TemplateColumn( template_code="{{ label }}:{{ record.col }}-{{ foo }}", extra_context={"label": "label"}, ) table = TestTable([{"col": "brad"}]) self.assertEqual(table.rows[0].get_cell("col_code"), "code:brad-") self.assertEqual(table.rows[0].get_cell("col_name"), "name:brad-empty\n") self.assertEqual(table.rows[0].get_cell("col_context"), "label:brad-") table.context = Context({"foo": "author"}) self.assertEqual(table.rows[0].get_cell("col_code"), "code:brad-author") self.assertEqual(table.rows[0].get_cell("col_name"), "name:brad-author\n") self.assertEqual(table.rows[0].get_cell("col_context"), "label:brad-author") # new table and render using the 'render_table' template tag. table = TestTable([{"col": "brad"}]) template = Template("{% load django_tables2 %}{% render_table table %}") html = template.render( Context({"request": build_request(), "table": table, "foo": "author"}) ) self.assertIn("name:brad-author\n", html) def test_should_support_default(self): class Table(tables.Table): foo = tables.TemplateColumn("default={{ default }}", default="bar") table = Table([{}]) self.assertEqual(table.rows[0].get_cell("foo"), "default=bar") def test_should_support_value(self): class Table(tables.Table): foo = tables.TemplateColumn("value={{ value }}") table = Table([{"foo": "bar"}]) self.assertEqual(table.rows[0].get_cell("foo"), "value=bar") template = Template("{% load django_tables2 %}{% render_table table %}") html = template.render(Context({"request": build_request(), "table": table})) self.assertIn("value=bar", html) def test_should_support_column(self): class Table(tables.Table): tcol = tables.TemplateColumn("column={{ column.name }}") table = Table([{"foo": "bar"}]) self.assertEqual(table.rows[0].get_cell("tcol"), "column=tcol") def test_should_raise_when_called_without_template(self): with self.assertRaises(ValueError): class Table(tables.Table): col = tables.TemplateColumn() def test_should_support_value_with_curly_braces(self): """ https://github.com/bradleyayers/django-tables2/issues/441 """ class Table(tables.Table): track = tables.TemplateColumn("track: {{ value }}") table = Table([{"track": "Beat it {Freestyle}"}]) self.assertEqual(table.rows[0].get_cell("track"), "track: Beat it {Freestyle}") def test_should_strip_tags_for_value(self): class Table(tables.Table): track = tables.TemplateColumn("{{ value }}") table = Table([{"track": "Space Oddity"}]) self.assertEqual(list(table.as_values()), [["Track"], ["Space Oddity"]]) django-tables2-2.1.1/tests/columns/test_datetimecolumn.py0000644000175000017500000000616513542204315023125 0ustar josephjosephfrom datetime import datetime import pytz from django.conf import settings from django.db import models from django.test import SimpleTestCase, override_settings import django_tables2 as tables def isoformat_link(value): return "/test/{}/".format(value.isoformat()) class DateTimeColumnTest(SimpleTestCase): """ Format string: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date D -- Day of the week, textual, 3 letters -- 'Fri' b -- Month, textual, 3 letters, lowercase -- 'jan' Y -- Year, 4 digits. -- '1999' A -- 'AM' or 'PM'. -- 'AM' f -- Time, in 12-hour hours[:minutes] -- '1', '1:30' """ def dt(self): dt = datetime(2012, 9, 11, 12, 30, 0) return pytz.timezone(settings.TIME_ZONE).localize(dt) def test_should_handle_explicit_format(self): class TestTable(tables.Table): date = tables.DateTimeColumn(format="D b Y") date_linkify = tables.DateTimeColumn( format="D b Y", accessor="date", linkify=isoformat_link ) class Meta: default = "—" table = TestTable([{"date": self.dt()}, {"date": None}]) self.assertEqual(table.rows[0].get_cell("date"), "Tue sep 2012") self.assertEqual( table.rows[0].get_cell("date_linkify"), 'Tue sep 2012', ) self.assertEqual(table.rows[1].get_cell("date"), "—") @override_settings(DATETIME_FORMAT="D Y b A f") def test_should_handle_long_format(self): class TestTable(tables.Table): date = tables.DateTimeColumn(short=False) class Meta: default = "—" table = TestTable([{"date": self.dt()}, {"date": None}]) self.assertEqual(table.rows[0].get_cell("date"), "Tue 2012 sep PM 12:30") self.assertEqual(table.rows[1].get_cell("date"), "—") @override_settings(SHORT_DATETIME_FORMAT="b Y D A f") def test_should_handle_short_format(self): class TestTable(tables.Table): date = tables.DateTimeColumn(short=True) class Meta: default = "—" table = TestTable([{"date": self.dt()}, {"date": None}]) self.assertEqual(table.rows[0].get_cell("date"), "sep 2012 Tue PM 12:30") self.assertEqual(table.rows[1].get_cell("date"), "—") def test_should_be_used_for_datetimefields(self): class DateTimeModel(models.Model): field = models.DateTimeField() class Meta: app_label = "django_tables2_test" class Table(tables.Table): class Meta: model = DateTimeModel self.assertIsInstance(Table.base_columns["field"], tables.DateTimeColumn) @override_settings(SHORT_DATETIME_FORMAT="b Y D A f") def test_value_returns_a_raw_value_without_html(self): class Table(tables.Table): col = tables.DateTimeColumn() table = Table([{"col": self.dt()}]) self.assertEqual(table.rows[0].get_cell_value("col"), "sep 2012 Tue PM 12:30") django-tables2-2.1.1/tests/columns/test_timecolumn.py0000644000175000017500000000244113542204315022260 0ustar josephjosephfrom datetime import time from django.db import models from django.test import SimpleTestCase import django_tables2 as tables class TimeColumnTest(SimpleTestCase): """ Format string for TimeColumn: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date """ def test_should_handle_explicit_format(self): class TestTable(tables.Table): time = tables.TimeColumn(format="H:i:s") class Meta: default = "—" table = TestTable([{"time": time(11, 11, 11)}, {"time": None}]) assert table.rows[0].get_cell("time") == "11:11:11" assert table.rows[1].get_cell("time") == "—" def test_should_be_used_for_timefields(self): class TimeModel(models.Model): field = models.TimeField() class Meta: app_label = "django_tables2_test" class Table(tables.Table): class Meta: model = TimeModel assert type(Table.base_columns["field"]) == tables.TimeColumn def test_value_returns_a_raw_value_without_html(self): class Table(tables.Table): col = tables.TimeColumn(format="H:i:s") table = Table([{"col": time(11, 11, 11)}]) assert table.rows[0].get_cell_value("col") == "11:11:11" django-tables2-2.1.1/tests/columns/test_filecolumn.py0000644000175000017500000000622113542204315022241 0ustar josephjosephimport os from django.core.files.base import ContentFile from django.core.files.storage import FileSystemStorage from django.db import models from django.db.models.fields.files import FieldFile from django.test import SimpleTestCase import django_tables2 as tables from ..utils import parse def storage(): """Provide a storage that exposes the test templates""" root = os.path.join(os.path.dirname(__file__), "..", "app", "templates") return FileSystemStorage(location=root, base_url="/baseurl/") def column(): return tables.FileColumn(attrs={"span": {"class": "span"}, "a": {"class": "a"}}) class FileColumnTest(SimpleTestCase): def test_should_be_used_for_filefields(self): class FileModel(models.Model): field = models.FileField() class Meta: app_label = "django_tables2_test" class Table(tables.Table): class Meta: model = FileModel self.assertEqual(type(Table.base_columns["field"]), tables.FileColumn) def test_filecolumn_supports_storage_file(self): file_ = storage().open("child/foo.html") try: root = parse(column().render(value=file_, record=None)) finally: file_.close() self.assertEqual(root.tag, "span") self.assertEqual(root.attrib, {"class": "span exists", "title": file_.name}) self.assertEqual(root.text, "foo.html") def test_filecolumn_supports_contentfile(self): name = "foobar.html" file_ = ContentFile("") file_.name = name root = parse(column().render(value=file_, record=None)) self.assertEqual(root.tag, "span") self.assertEqual(root.attrib, {"title": name, "class": "span"}) self.assertEqual(root.text, "foobar.html") def test_filecolumn_supports_fieldfile(self): field = models.FileField(storage=storage()) name = "child/foo.html" class Table(tables.Table): filecolumn = column() table = Table([{"filecolumn": FieldFile(instance=None, field=field, name=name)}]) html = table.rows[0].get_cell("filecolumn") root = parse(html) self.assertEqual(root.tag, "a") self.assertEqual(root.attrib, {"class": "a", "href": "/baseurl/child/foo.html"}) span = root.find("span") self.assertEqual(span.tag, "span") self.assertEqual(span.text, "foo.html") # Now try a file that doesn't exist name = "child/does_not_exist.html" fieldfile = FieldFile(instance=None, field=field, name=name) root = parse(column().render(value=fieldfile, record=None)) self.assertEqual(root.tag, "span") self.assertEqual(root.attrib, {"class": "span missing", "title": name}) self.assertEqual(root.text, "does_not_exist.html") def test_filecolumn_text_custom_value(self): file_ = ContentFile("") file_.name = "foobar.html" root = parse(tables.FileColumn(text="Download").render(value=file_, record=None)) self.assertEqual(root.tag, "span") self.assertEqual(root.attrib, {"title": file_.name, "class": ""}) self.assertEqual(root.text, "Download") django-tables2-2.1.1/tests/columns/test_general.py0000644000175000017500000004501313542204315021523 0ustar josephjosephfrom django.core.exceptions import ImproperlyConfigured from django.db import models from django.test import TestCase from django.utils.safestring import SafeData, mark_safe from django.utils.translation import gettext_lazy import django_tables2 as tables from ..app.models import Person from ..utils import build_request, parse request = build_request("/") class ColumnGeneralTest(TestCase): def test_column_render_supports_kwargs(self): class TestColumn(tables.Column): def render(self, **kwargs): return set(kwargs.keys()) class TestTable(tables.Table): foo = TestColumn() table = TestTable([{"foo": "bar"}]) expected = {"record", "value", "column", "bound_column", "bound_row", "table"} self.assertEqual(table.rows[0].get_cell("foo"), expected) def test_column_header_should_use_titlised_verbose_name_unless_given_explicitly(self): class SimpleTable(tables.Table): basic = tables.Column() acronym = tables.Column(verbose_name="has FBI help") table = SimpleTable([]) self.assertEqual(table.columns["basic"].header, "Basic") self.assertEqual(table.columns["acronym"].header, "has FBI help") def test_should_support_safe_verbose_name(self): class SimpleTable(tables.Table): safe = tables.Column(verbose_name=mark_safe("Safe")) table = SimpleTable([]) self.assertIsInstance(table.columns["safe"].header, SafeData) def test_should_raise_on_invalid_accessor(self): with self.assertRaises(TypeError): class SimpleTable(tables.Table): column = tables.Column(accessor={}) def test_column_with_callable_accessor_should_not_have_default(self): with self.assertRaises(TypeError): class SimpleTable(tables.Table): column = tables.Column(accessor=lambda: "foo", default="") def test_should_support_safe_verbose_name_via_model(self): class PersonTable(tables.Table): safe = tables.Column() table = PersonTable(Person.objects.all()) self.assertIsInstance(table.columns["safe"].header, SafeData) def test_should_support_empty_string_as_explicit_verbose_name(self): class SimpleTable(tables.Table): acronym = tables.Column(verbose_name="") table = SimpleTable([]) self.assertEqual(table.columns["acronym"].header, "") def test_handle_verbose_name_of_many2onerel(self): class Table(tables.Table): count = tables.Column(accessor="info_list.count") Person.objects.create(first_name="bradley", last_name="ayers") table = Table(Person.objects.all()) self.assertEqual(table.columns["count"].verbose_name, "Information") def test_orderable(self): class SimpleTable(tables.Table): name = tables.Column() table = SimpleTable([]) self.assertTrue(table.columns["name"].orderable) class SimpleTable(tables.Table): name = tables.Column() class Meta: orderable = False table = SimpleTable([]) self.assertFalse(table.columns["name"].orderable) class SimpleTable(tables.Table): name = tables.Column() class Meta: orderable = True table = SimpleTable([]) self.assertTrue(table.columns["name"].orderable) def test_order_by_defaults_to_accessor(self): class SimpleTable(tables.Table): foo = tables.Column(accessor="bar") table = SimpleTable([]) self.assertEqual(table.columns["foo"].order_by, ("bar",)) def test_supports_order_by(self): class SimpleTable(tables.Table): name = tables.Column(order_by=("last_name", "-first_name")) age = tables.Column() table = SimpleTable([], order_by=("-age",)) # alias self.assertEqual(table.columns["name"].order_by_alias, "name") self.assertEqual(table.columns["age"].order_by_alias, "-age") # order by self.assertEqual(table.columns["name"].order_by, ("last_name", "-first_name")) self.assertEqual(table.columns["age"].order_by, ("-age",)) # now try with name ordered table = SimpleTable([], order_by=("-name",)) # alias self.assertEqual(table.columns["name"].order_by_alias, "-name") self.assertEqual(table.columns["age"].order_by_alias, "age") # alias next self.assertEqual(table.columns["name"].order_by_alias.next, "name") self.assertEqual(table.columns["age"].order_by_alias.next, "age") # order by self.assertEqual(table.columns["name"].order_by, ("-last_name", "first_name")) self.assertEqual(table.columns["age"].order_by, ("age",)) def test_supports_is_ordered(self): class SimpleTable(tables.Table): name = tables.Column() # sorted table = SimpleTable([], order_by="name") self.assertTrue(table.columns["name"].is_ordered) # unsorted table = SimpleTable([]) self.assertFalse(table.columns["name"].is_ordered) def test_translation(self): """ Tests different types of values for the ``verbose_name`` property of a column. """ class TranslationTable(tables.Table): text = tables.Column(verbose_name=gettext_lazy("Text")) table = TranslationTable([]) self.assertEqual(table.columns["text"].header, "Text") def test_sequence(self): """ Ensures that the sequence of columns is configurable. """ class TestTable(tables.Table): a = tables.Column() b = tables.Column() c = tables.Column() self.assertEqual(["a", "b", "c"], TestTable([]).columns.names()) self.assertEqual(["b", "a", "c"], TestTable([], sequence=("b", "a", "c")).columns.names()) class TestTable2(TestTable): class Meta: sequence = ("b", "a", "c") self.assertEqual(["b", "a", "c"], TestTable2([]).columns.names()) self.assertEqual(["a", "b", "c"], TestTable2([], sequence=("a", "b", "c")).columns.names()) class TestTable3(TestTable): class Meta: sequence = ("c",) self.assertEqual(["c", "a", "b"], TestTable3([]).columns.names()) self.assertEqual(["c", "a", "b"], TestTable([], sequence=("c",)).columns.names()) class TestTable4(TestTable): class Meta: sequence = ("...",) self.assertEqual(["a", "b", "c"], TestTable4([]).columns.names()) self.assertEqual(["a", "b", "c"], TestTable([], sequence=("...",)).columns.names()) class TestTable5(TestTable): class Meta: sequence = ("b", "...") self.assertEqual(["b", "a", "c"], TestTable5([]).columns.names()) self.assertEqual(["b", "a", "c"], TestTable([], sequence=("b", "...")).columns.names()) class TestTable6(TestTable): class Meta: sequence = ("...", "b") self.assertEqual(["a", "c", "b"], TestTable6([]).columns.names()) self.assertEqual(["a", "c", "b"], TestTable([], sequence=("...", "b")).columns.names()) class TestTable7(TestTable): class Meta: sequence = ("b", "...", "a") self.assertEqual(["b", "c", "a"], TestTable7([]).columns.names()) self.assertEqual(["b", "c", "a"], TestTable([], sequence=("b", "...", "a")).columns.names()) # Let's test inheritence class TestTable8(TestTable): d = tables.Column() e = tables.Column() f = tables.Column() class Meta: sequence = ("d", "...") class TestTable9(TestTable): d = tables.Column() e = tables.Column() f = tables.Column() self.assertEqual(["d", "a", "b", "c", "e", "f"], TestTable8([]).columns.names()) self.assertEqual( ["d", "a", "b", "c", "e", "f"], TestTable9([], sequence=("d", "...")).columns.names() ) def test_should_support_both_meta_sequence_and_constructor_exclude(self): """ Issue #32 describes a problem when both ``Meta.sequence`` and ``Table(..., exclude=...)`` are used on a single table. The bug caused an exception to be raised when the table was iterated. """ class SequencedTable(tables.Table): a = tables.Column() b = tables.Column() c = tables.Column() class Meta: sequence = ("a", "...") table = SequencedTable([], exclude=("c",)) table.as_html(request) self.assertEqual(table.columns.names(), ["a", "b"]) def test_bound_columns_should_support_indexing(self): class SimpleTable(tables.Table): a = tables.Column() b = tables.Column() table = SimpleTable([]) self.assertEqual(table.columns[1].name, "b") self.assertEqual(table.columns["b"].name, "b") def test_cell_attrs_applies_to_td_and_th_and_footer_td(self): class SimpleTable(tables.Table): a = tables.Column( attrs={"cell": {"key": "value"}}, footer=lambda table: len(table.data) ) # providing data ensures 1 row is rendered table = SimpleTable([{"a": "value"}]) root = parse(table.as_html(request)) self.assertEqual( root.findall(".//thead/tr/th")[0].attrib, {"key": "value", "class": "orderable"} ) self.assertEqual(root.findall(".//tbody/tr/td")[0].attrib, {"key": "value"}) self.assertEqual(root.findall(".//tfoot/tr/td")[0].attrib, {"key": "value"}) def test_th_are_given_orderable_class_if_column_is_orderable(self): class SimpleTable(tables.Table): a = tables.Column() b = tables.Column(orderable=False) table = SimpleTable([{"a": "value"}]) root = parse(table.as_html(request)) # return classes of an element as a set classes = lambda x: set(x.attrib.get("class", "").split()) self.assertIn("orderable", classes(root.findall(".//thead/tr/th")[0])) self.assertNotIn("orderable", classes(root.findall(".//thead/tr/th")[1])) # Now try with an ordered table table = SimpleTable([], order_by="a") root = parse(table.as_html(request)) # return classes of an element as a set self.assertIn("orderable", classes(root.findall(".//thead/tr/th")[0])) self.assertIn("asc", classes(root.findall(".//thead/tr/th")[0])) self.assertNotIn("orderable", classes(root.findall(".//thead/tr/th")[1])) def test_empty_values_triggers_default(self): class Table(tables.Table): a = tables.Column(empty_values=(1, 2), default="--") table = Table([{"a": 1}, {"a": 2}, {"a": 3}, {"a": 4}]) self.assertEqual([row.get_cell("a") for row in table.rows], ["--", "--", 3, 4]) def test_register_skips_non_columns(self): from django_tables2.columns.base import library currently_registered = len(library.columns) with self.assertRaises(ImproperlyConfigured): @library.register class Klass(object): pass self.assertEqual(len(library.columns), currently_registered) def test_raises_when_using_non_supported_index(self): class Table(tables.Table): column = tables.Column() table = Table([{"column": "foo"}]) row = table.rows[0] with self.assertRaises(TypeError): row[table] def test_related_fields_get_correct_type(self): """ Types of related fields should also lead to the correct type of column. """ class PersonTable(tables.Table): class Meta: model = Person fields = ["first_name", "occupation.boolean"] table = PersonTable([]) self.assertEqual( [type(column).__name__ for column in table.base_columns.values()], ["Column", "BooleanColumn"], ) class MyModel(models.Model): item1 = models.CharField(max_length=10) class Meta: app_label = "django_tables2_tests" class MyTable(tables.Table): item1 = tables.Column(verbose_name="Nice column name") class Meta: model = MyModel fields = ("item1",) class ColumnInheritanceTest(TestCase): def test_column_params_should_be_preserved_under_inheritance(self): """ Github issue #337 Columns explicitly defined on MyTable get overridden by columns implicitly defined on it's child. If the column is not redefined, the explicit definition of MyTable is used, preserving the specialized verbose_name defined on it. """ class MyTableA(MyTable): """ having an empty `class Meta` should not undo the explicit definition of column item1 in MyTable. """ class Meta(MyTable.Meta): pass class MyTableB(MyTable): """ having a non-empty `class Meta` should not undo the explicit definition of column item1 in MyTable. """ class Meta(MyTable.Meta): per_page = 22 table = MyTable(MyModel.objects.all()) tableA = MyTableA(MyModel.objects.all()) tableB = MyTableB(MyModel.objects.all()) self.assertEqual(table.columns["item1"].verbose_name, "Nice column name") self.assertEqual(tableA.columns["item1"].verbose_name, "Nice column name") self.assertEqual(tableB.columns["item1"].verbose_name, "Nice column name") def test_explicit_column_can_be_overridden_by_other_explicit_column(self): class MyTableC(MyTable): """ If we define a new explict item1 column, that one should be used. """ item1 = tables.Column(verbose_name="New nice column name") table = MyTable(MyModel.objects.all()) tableC = MyTableC(MyModel.objects.all()) self.assertEqual(table.columns["item1"].verbose_name, "Nice column name") self.assertEqual(tableC.columns["item1"].verbose_name, "New nice column name") def test_override_column_class_names(self): """ We control the output of CSS class names for a column by overriding get_column_class_names """ class MyTable(tables.Table): population = tables.Column(verbose_name="Population") def get_column_class_names(self, classes_set, bound_column): classes_set.add("prefix-%s" % bound_column.name) return classes_set TEST_DATA = [ {"name": "Belgium", "population": 11200000}, {"name": "Luxembourgh", "population": 540000}, {"name": "France", "population": 66000000}, ] html = MyTable(TEST_DATA).as_html(build_request()) self.assertIn('11200000', html) class ColumnAttrsTest(TestCase): def setUp(self): Person.objects.create(first_name="Jan", last_name="Pietersz.") Person.objects.create(first_name="Sjon", last_name="Jansen") def test_computable_td_attrs(self): """Computable attrs for columns, using table argument""" class Table(tables.Table): person = tables.Column(attrs={"cell": {"data-length": lambda table: len(table.data)}}) first_name = tables.Column( attrs={"td": {"class": lambda table: "status-{}".format(len(table.data))}} ) table = Table(Person.objects.all()) html = table.as_html(request) # cell should affect both and self.assertIn('', html) self.assertIn('', html) # td should only affect self.assertIn('', html) def test_computable_td_attrs_defined_in_column_class_attribute(self): """Computable attrs for columns, using custom Column""" class MyColumn(tables.Column): attrs = {"td": {"data-test": lambda table: len(table.data)}} class Table(tables.Table): last_name = MyColumn() table = Table(Person.objects.all()) html = table.as_html(request) root = parse(html) self.assertEqual(root.findall(".//tbody/tr/td")[0].attrib, {"data-test": "2"}) self.assertEqual(root.findall(".//tbody/tr/td")[1].attrib, {"data-test": "2"}) def test_computable_td_attrs_defined_in_column_class_attribute_record(self): """Computable attrs for columns, using custom column""" class PersonColumn(tables.Column): attrs = { "td": { "data-first-name": lambda record: record.first_name, "data-last-name": lambda record: record.last_name, } } def render(self, record): return "{} {}".format(record.first_name, record.last_name) class Table(tables.Table): person = PersonColumn(empty_values=()) table = Table(Person.objects.all()) html = table.as_html(request) root = parse(html) self.assertEqual( root.findall(".//tbody/tr/td")[0].attrib, {"data-first-name": "Jan", "data-last-name": "Pietersz."}, ) def test_computable_column_td_attrs_record_header(self): """ Computable attrs for columns, using custom column with a callable containing a catch-all argument. """ def data_first_name(**kwargs): record = kwargs.get("record", None) return "header" if not record else record.first_name class Table(tables.Table): first_name = tables.Column( attrs={ "cell": { "data-first-name": data_first_name, "class": lambda value: "status-{}".format(value), } } ) table = Table(Person.objects.all()) html = table.as_html(request) root = parse(html) self.assertEqual( root.findall(".//thead/tr/th")[0].attrib, {"class": "orderable", "data-first-name": "header"}, ) self.assertEqual( root.findall(".//tbody/tr/td")[0].attrib, {"class": "status-Jan", "data-first-name": "Jan"}, ) self.assertEqual( root.findall(".//tbody/tr/td")[1].attrib, {"class": "status-Sjon", "data-first-name": "Sjon"}, ) django-tables2-2.1.1/tests/test_rows.py0000644000175000017500000001175213542204315017423 0ustar josephjosephfrom itertools import count from django.db import models from django.test import SimpleTestCase import django_tables2 as tables class RowsTest(SimpleTestCase): def test_bound_rows(self): class SimpleTable(tables.Table): name = tables.Column() data = [{"name": "Bradley"}, {"name": "Chris"}, {"name": "Davina"}] table = SimpleTable(data) # iteration records = [] for row in table.rows: records.append(row.record) self.assertEqual(records, data) def test_bound_row(self): class SimpleTable(tables.Table): name = tables.Column() occupation = tables.Column() age = tables.Column() record = {"name": "Bradley", "age": 20, "occupation": "programmer"} table = SimpleTable([record]) row = table.rows[0] # integer indexing into a row self.assertEqual(row.get_cell(0), record["name"]) self.assertEqual(row.get_cell(1), record["occupation"]) self.assertEqual(row.get_cell(2), record["age"]) with self.assertRaises(IndexError): row.get_cell(3) # column name indexing into a row self.assertEqual(row.get_cell("name"), record["name"]) self.assertEqual(row.get_cell("occupation"), record["occupation"]) self.assertEqual(row.get_cell("age"), record["age"]) with self.assertRaises(KeyError): row.get_cell("gamma") # row should support contains check self.assertIn("name", row) self.assertIn("occupation", row) self.assertNotIn("gamma", row) def test_boud_row_cells(self): class SimpleTable(tables.Table): name = tables.Column() occupation = tables.Column() age = tables.Column() record = {"name": "Bradley", "age": 20, "occupation": "programmer"} table = SimpleTable([record]) row = table.rows[0] self.assertEqual(row.cells.name, record["name"]) self.assertEqual(row.cells.age, record["age"]) self.assertEqual(row.cells.name, row.get_cell("name")) self.assertEqual(row.cells[0], record["name"]) self.assertEqual(row.cells[0], row.get_cell(0)) with self.assertRaises(IndexError): row.cells[3] with self.assertRaises(KeyError): row.cells["gamma"] def test_row_attrs(self): """ If a callable returns an empty string, do not add a space to the CSS class attribute. (#416) """ counter = count() class Table(tables.Table): name = tables.Column() class Meta: row_attrs = {"class": lambda: "" if next(counter) % 2 == 0 else "bla"} table = Table([{"name": "Brian"}, {"name": "Thomas"}, {"name": "John"}]) self.assertEqual(table.rows[0].attrs["class"], "even") self.assertEqual(table.rows[1].attrs["class"], "bla odd") self.assertEqual(table.rows[1].attrs["class"], "even") def test_get_cell_display(self): class A(models.Model): foo = models.CharField(max_length=1, choices=(("a", "valA"), ("b", "valB"))) class Meta: app_label = "tests" class B(models.Model): a = models.ForeignKey(A, on_delete=models.CASCADE) class Meta: app_label = "tests" class C(models.Model): b = models.ForeignKey(B, on_delete=models.CASCADE) class Meta: app_label = "tests" class Tab(tables.Table): a = tables.Column(accessor="b.a.foo") class Meta: model = C a = A(foo="a") b = B(a=a) c = C(b=b) tab = Tab([c]) row = tab.rows[0] self.assertEqual(row.get_cell("a"), "valA") def test_even_odd_css_class(self): """ Test for BoundRow.get_even_odd_css_class() method """ class SimpleTable(tables.Table): foo = tables.Column() def get_top_pinned_data(self): return [{"foo": "top-pinned"}] def get_bottom_pinned_data(self): return [{"foo": "bottom-pinned"}] data = [{"foo", "bar"}, {"foo", "bas"}, {"foo", "baz"}] simple_table = SimpleTable(data) count = 0 prev = None for row in simple_table.rows: if prev: self.assertNotEqual(row.get_even_odd_css_class(), prev.get_even_odd_css_class()) prev = row count += 1 # count should be 5 because: # - First row is a top pinned row. # - Three defaults rows with data. # - Last row is a bottom pinned row. self.assertEqual(count, 5) # Important! # Length of data is five because pinned rows are added to data list. # If pinned rows are added only in the iteration on BoundRows, # then nothing will display if there are *only* pinned rows self.assertEqual(len(simple_table.rows), 5) django-tables2-2.1.1/tests/test_core.py0000644000175000017500000005601713542204315017364 0ustar josephjoseph"""Test the core table functionality.""" import copy import itertools from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.test import SimpleTestCase, TestCase, override_settings from django.utils.translation import gettext_lazy, override import django_tables2 as tables from django_tables2.tables import DeclarativeColumnsMetaclass from .app.models import Occupation, Person from .utils import build_request, parse request = build_request("/") MEMORY_DATA = [ {"i": 2, "alpha": "b", "beta": "b"}, {"i": 1, "alpha": "a", "beta": "c"}, {"i": 3, "alpha": "c", "beta": "a"}, ] class UnorderedTable(tables.Table): i = tables.Column() alpha = tables.Column() beta = tables.Column() class CoreTest(SimpleTestCase): def test_omitting_data(self): with self.assertRaises(TypeError): UnorderedTable() def test_column_named_items(self): """A column named items must not make the table fail.""" # https://github.com/bradleyayers/django-tables2/issues/316 class ItemsTable(tables.Table): items = tables.Column() table = ItemsTable([{"items": 123}, {"items": 2345}]) html = table.as_html(request) self.assertIn("123", html) self.assertIn("2345", html) def test_declarations(self): """Test defining tables by declaration.""" class GeoAreaTable(tables.Table): name = tables.Column() population = tables.Column() self.assertEqual(len(GeoAreaTable.base_columns), 2) self.assertIn("name", GeoAreaTable.base_columns) self.assertFalse(hasattr(GeoAreaTable, "name")) class CountryTable(GeoAreaTable): capital = tables.Column() self.assertEqual(len(CountryTable.base_columns), 3) self.assertIn("capital", CountryTable.base_columns) # multiple inheritance class AddedMixin(tables.Table): added = tables.Column() class CityTable(GeoAreaTable, AddedMixin): mayor = tables.Column() self.assertEqual(len(CityTable.base_columns), 4) self.assertIn("added", CityTable.base_columns) # overwrite a column with a non-column class MayorlessCityTable(CityTable): mayor = None self.assertEqual(len(MayorlessCityTable.base_columns), 3) def test_metaclass_inheritance(self): class Tweaker(type): """Adds an attribute "tweaked" to all classes""" def __new__(cls, name, bases, attrs): attrs["tweaked"] = True return super().__new__(cls, name, bases, attrs) class Meta(Tweaker, DeclarativeColumnsMetaclass): pass class TweakedTableBase(tables.Table): __metaclass__ = Meta name = tables.Column() TweakedTable = Meta("TweakedTable", (TweakedTableBase,), {}) table = TweakedTable([]) self.assertIn("name", table.columns) self.assertTrue(table.tweaked) # now flip the order class FlippedMeta(DeclarativeColumnsMetaclass, Tweaker): pass class FlippedTweakedTableBase(tables.Table): name = tables.Column() FlippedTweakedTable = FlippedMeta("FlippedTweakedTable", (FlippedTweakedTableBase,), {}) table = FlippedTweakedTable([]) self.assertIn("name", table.columns) self.assertTrue(table.tweaked) def test_Meta_attribute_incorrect_types(self): with self.assertRaises(TypeError): class MetaTable1(tables.Table): class Meta: exclude = "foo" with self.assertRaises(TypeError): class MetaTable2(tables.Table): class Meta: sequence = "..." with self.assertRaises(TypeError): class MetaTable3(tables.Table): class Meta: model = {} def test_table_attrs(self): class TestTable(tables.Table): class Meta: attrs = {} self.assertEqual(TestTable([]).attrs.as_html(), "") class TestTable2(tables.Table): class Meta: attrs = {"a": "b"} self.assertEqual(TestTable2([]).attrs.as_html(), 'a="b"') class TestTable3(tables.Table): pass self.assertEqual(TestTable3([]).attrs.as_html(), "") self.assertEqual(TestTable3([], attrs={"a": "b"}).attrs.as_html(), 'a="b"') class TestTable4(tables.Table): class Meta: attrs = {"a": "b"} self.assertEqual(TestTable4([], attrs={"c": "d"}).attrs.as_html(), 'c="d"') def test_attrs_support_computed_values(self): counter = itertools.count() class TestTable(tables.Table): class Meta: attrs = {"id": lambda: "test_table_%d" % next(counter)} self.assertEqual('id="test_table_0"', TestTable([]).attrs.as_html()) self.assertEqual('id="test_table_1"', TestTable([]).attrs.as_html()) @override_settings(DJANGO_TABLES2_TABLE_ATTRS={"class": "table-compact"}) def test_attrs_from_settings(self): class Table(tables.Table): pass table = Table({}) self.assertEqual(table.attrs.as_html(), 'class="table-compact"') def test_table_attrs_thead_tbody_tfoot(self): class Table(tables.Table): column = tables.Column(footer="foo") class Meta: attrs = { "class": "table-class", "thead": {"class": "thead-class"}, "tbody": {"class": "tbody-class"}, "tfoot": {"class": "tfoot-class"}, } html = Table([]).as_html(build_request()) self.assertIn('', html) self.assertIn('', html) self.assertIn('', html) self.assertIn('', html) def test_datasource_untouched(self): """Ensure that data the data datasource is not modified by table operations.""" original_data = copy.deepcopy(MEMORY_DATA) table = UnorderedTable(MEMORY_DATA) table.order_by = "i" list(table.rows) self.assertEqual(MEMORY_DATA, original_data) table = UnorderedTable(MEMORY_DATA) table.order_by = "beta" list(table.rows) self.assertEqual(MEMORY_DATA, original_data) def test_should_support_tuple_data_source(self): class SimpleTable(tables.Table): name = tables.Column() table = SimpleTable(({"name": "brad"}, {"name": "davina"})) self.assertEqual(len(table.rows), 2) def test_column_count(self): class SimpleTable(tables.Table): visible = tables.Column(visible=True) hidden = tables.Column(visible=False) # The columns container supports the len() builtin self.assertEqual(len(SimpleTable([]).columns), 1) def test_column_accessor(self): class SimpleTable(UnorderedTable): col1 = tables.Column(accessor="alpha.upper.isupper") col2 = tables.Column(accessor="alpha.upper") table = SimpleTable(MEMORY_DATA) self.assertTrue(table.rows[0].get_cell("col1")) self.assertEqual(table.rows[0].get_cell("col2"), "B") def test_exclude_columns(self): """ Defining ``Table.Meta.exclude`` or providing an ``exclude`` argument when instantiating a table should have the same effect -- exclude those columns from the table. It should have the same effect as not defining the columns originally. """ table = UnorderedTable([], exclude=("i")) self.assertEqual(table.columns.names(), ["alpha", "beta"]) # Table.Meta: exclude=... class PartialTable(UnorderedTable): class Meta: exclude = ("alpha",) table = PartialTable([]) self.assertEqual(table.columns.names(), ["i", "beta"]) # Inheritence -- exclude in parent, add in child class AddonTable(PartialTable): added = tables.Column() table = AddonTable([]) self.assertEqual(table.columns.names(), ["i", "beta", "added"]) # Inheritence -- exclude in child class ExcludeTable(UnorderedTable): added = tables.Column() class Meta: exclude = ("beta",) table = ExcludeTable([]) self.assertEqual(table.columns.names(), ["i", "alpha", "added"]) def test_table_exclude_property_should_override_constructor_argument(self): class SimpleTable(tables.Table): a = tables.Column() b = tables.Column() table = SimpleTable([], exclude=("b",)) self.assertEqual(table.columns.names(), ["a"]) table.exclude = ("a",) self.assertEqual(table.columns.names(), ["b"]) def test_exclude_should_work_on_sequence_too(self): """It should be possible to define a sequence on a table and exclude it in a child of that table.""" class PersonTable(tables.Table): first_name = tables.Column() last_name = tables.Column() occupation = tables.Column() class Meta: sequence = ("first_name", "last_name", "occupation") class AnotherPersonTable(PersonTable): class Meta(PersonTable.Meta): exclude = ("first_name", "last_name") tableA = PersonTable([]) self.assertEqual(tableA.columns.names(), ["first_name", "last_name", "occupation"]) tableB = AnotherPersonTable([]) self.assertEqual(tableB.columns.names(), ["occupation"]) tableC = PersonTable([], exclude=("first_name")) self.assertEqual(tableC.columns.names(), ["last_name", "occupation"]) def test_pagination(self): class BookTable(tables.Table): name = tables.Column() # create some sample data data = list([{"name": "Book No. %d" % i} for i in range(100)]) books = BookTable(data) # external paginator paginator = Paginator(books.rows, 10) self.assertEqual(paginator.num_pages, 10) page = paginator.page(1) self.assertFalse(page.has_previous()) self.assertTrue(page.has_next()) # integrated paginator books.paginate(page=1) self.assertTrue(hasattr(books, "page")) books.paginate(page=1, per_page=10) self.assertEqual(len(list(books.page.object_list)), 10) # new attributes self.assertEqual(books.paginator.num_pages, 10) self.assertFalse(books.page.has_previous()) self.assertTrue(books.page.has_next()) # accessing a non-existant page raises 404 with self.assertRaises(EmptyPage): books.paginate(Paginator, page=9999, per_page=10) with self.assertRaises(PageNotAnInteger): books.paginate(Paginator, page="abc", per_page=10) def test_pagination_shouldnt_prevent_multiple_rendering(self): class SimpleTable(tables.Table): name = tables.Column() table = SimpleTable([{"name": "brad"}]) table.paginate() self.assertEqual(table.as_html(request), table.as_html(request)) def test_empty_text(self): class TestTable(tables.Table): a = tables.Column() table = TestTable([]) self.assertEqual(table.empty_text, None) class TestTable2(tables.Table): a = tables.Column() class Meta: empty_text = "nothing here" table = TestTable2([]) self.assertEqual(table.empty_text, "nothing here") table = TestTable2([], empty_text="still nothing") self.assertEqual(table.empty_text, "still nothing") def test_empty_text_gettext_lazy(self): class TestTable(tables.Table): a = tables.Column() class Meta: empty_text = gettext_lazy("next") table = TestTable([]) self.assertEqual(table.empty_text, "next") with override("nl"): table = TestTable([]) self.assertEqual(table.empty_text, "volgende") def test_prefix(self): """Verify table prefixes affect the names of querystring parameters.""" class TableA(tables.Table): name = tables.Column() class Meta: prefix = "x" table = TableA([]) html = table.as_html(build_request("/")) self.assertEqual("x", table.prefix) self.assertIn("xsort=name", html) class TableB(tables.Table): last_name = tables.Column() self.assertEqual("", TableB([]).prefix) self.assertEqual("x", TableB([], prefix="x").prefix) table = TableB([]) table.prefix = "x-" html = table.as_html(build_request("/")) self.assertEqual("x-", table.prefix) self.assertIn("x-sort=last_name", html) def test_field_names(self): class TableA(tables.Table): class Meta: order_by_field = "abc" page_field = "def" per_page_field = "ghi" table = TableA([]) self.assertEqual("abc", table.order_by_field) self.assertEqual("def", table.page_field) self.assertEqual("ghi", table.per_page_field) def test_field_names_with_prefix(self): class TableA(tables.Table): class Meta: order_by_field = "sort" page_field = "page" per_page_field = "per_page" prefix = "1-" table = TableA([]) self.assertEqual("1-sort", table.prefixed_order_by_field) self.assertEqual("1-page", table.prefixed_page_field) self.assertEqual("1-per_page", table.prefixed_per_page_field) class TableB(tables.Table): class Meta: order_by_field = "sort" page_field = "page" per_page_field = "per_page" table = TableB([], prefix="1-") self.assertEqual("1-sort", table.prefixed_order_by_field) self.assertEqual("1-page", table.prefixed_page_field) self.assertEqual("1-per_page", table.prefixed_per_page_field) table = TableB([]) table.prefix = "1-" self.assertEqual("1-sort", table.prefixed_order_by_field) self.assertEqual("1-page", table.prefixed_page_field) self.assertEqual("1-per_page", table.prefixed_per_page_field) def test_should_support_a_template_name_to_be_specified(self): class ConstructorSpecifiedTemplateTable(tables.Table): name = tables.Column() table = ConstructorSpecifiedTemplateTable([], template_name="dummy.html") self.assertEqual(table.template_name, "dummy.html") class PropertySpecifiedTemplateTable(tables.Table): name = tables.Column() table = PropertySpecifiedTemplateTable([]) table.template_name = "dummy.html" self.assertEqual(table.template_name, "dummy.html") class DefaultTable(tables.Table): pass table = DefaultTable([]) self.assertEqual(table.template_name, "django_tables2/table.html") def test_template_name_in_meta_class_declaration_should_be_honored(self): class MetaDeclarationSpecifiedTemplateTable(tables.Table): name = tables.Column() class Meta: template_name = "dummy.html" table = MetaDeclarationSpecifiedTemplateTable([]) self.assertEqual(table.template_name, "dummy.html") self.assertEqual(table.as_html(request), "dummy template contents\n") def test_should_support_rendering_multiple_times(self): class MultiRenderTable(tables.Table): name = tables.Column() # test list data table = MultiRenderTable([{"name": "brad"}]) self.assertEqual(table.as_html(request), table.as_html(request)) def test_column_defaults_are_honored(self): class Table(tables.Table): name = tables.Column(default="abcd") class Meta: default = "efgh" table = Table([{}], default="ijkl") self.assertEqual(table.rows[0].get_cell("name"), "abcd") def test_table_meta_defaults_are_honored(self): class Table(tables.Table): name = tables.Column() class Meta: default = "abcd" table = Table([{}]) self.assertEqual(table.rows[0].get_cell("name"), "abcd") def test_table_defaults_are_honored(self): class Table(tables.Table): name = tables.Column() table = Table([{}], default="abcd") self.assertEqual(table.rows[0].get_cell("name"), "abcd") table = Table([{}], default="abcd") table.default = "efgh" self.assertEqual(table.rows[0].get_cell("name"), "efgh") class BoundColumnTest(SimpleTestCase): def test_attrs_bool_error(self): class Table(tables.Table): c_element = tables.Column() class ErrorObject: def __bool__(self): raise NotImplementedError table = Table([{"c_element": ErrorObject()}]) list(table.rows[0].items()) try: table.columns[0].attrs except NotImplementedError: self.fail("__bool__ should not be evaluated!") def test_attrs_falsy_object(self): """Computed attrs in BoundColumn should be passed the column value, even if its __bool__ returns False. """ class Table(tables.Table): c_element = tables.Column() class Meta: attrs = {"td": {"data-column-name": lambda value: value.name}} class FalsyObject: name = "FalsyObject1" def __bool__(self): return False table = Table([{"c_element": FalsyObject()}]) list(table.rows[0].items()) self.assertEqual("FalsyObject1", table.columns[0].attrs["td"]["data-column-name"]) class AsValuesTest(TestCase): AS_VALUES_DATA = [ {"name": "Adrian", "country": "Australia"}, {"name": "Adrian", "country": "Brazil"}, {"name": "Audrey", "country": "Chile"}, {"name": "Bassie", "country": "Belgium"}, ] def test_as_values(self): class Table(tables.Table): name = tables.Column() country = tables.Column() expected = [["Name", "Country"]] + [[r["name"], r["country"]] for r in self.AS_VALUES_DATA] table = Table(self.AS_VALUES_DATA) self.assertEqual(list(table.as_values()), expected) def test_as_values_exclude(self): class Table(tables.Table): name = tables.Column() country = tables.Column() expected = [["Name"]] + [[r["name"]] for r in self.AS_VALUES_DATA] table = Table(self.AS_VALUES_DATA) self.assertEqual(list(table.as_values(exclude_columns=("country",))), expected) def test_as_values_exclude_from_export(self): class Table(tables.Table): name = tables.Column() buttons = tables.Column(exclude_from_export=True) self.assertEqual(list(Table([]).as_values()), [["Name"]]) def test_as_values_visible_False(self): class Table(tables.Table): name = tables.Column() website = tables.Column(visible=False) self.assertEqual(list(Table([]).as_values()), [["Name", "Website"]]) def test_as_values_empty_values(self): """Table's as_values() method returns `None` for missing values.""" class Table(tables.Table): name = tables.Column() country = tables.Column() data = [ {"name": "Adrian", "country": "Brazil"}, {"name": "Audrey"}, {"name": "Bassie", "country": "Belgium"}, {"country": "France"}, ] expected = [["Name", "Country"]] + [[r.get("name"), r.get("country")] for r in data] table = Table(data) self.assertEqual(list(table.as_values()), expected) def test_render_FOO_exception(self): message = "Custom render-method fails" class Table(tables.Table): country = tables.Column() def render_country(self, value): raise Exception(message) return value + " test" with self.assertRaisesMessage(Exception, message): Table(self.AS_VALUES_DATA).as_html(build_request()) def test_as_values_render_FOO(self): class Table(tables.Table): name = tables.Column() country = tables.Column() def render_country(self, value): return value + " test" expected = [["Name", "Country"]] + [ [r["name"], r["country"] + " test"] for r in self.AS_VALUES_DATA ] self.assertEqual(list(Table(self.AS_VALUES_DATA).as_values()), expected) def test_as_values_value_FOO(self): class Table(tables.Table): name = tables.Column() country = tables.Column() def render_country(self, value): return value + " test" def value_country(self, value): return value + " different" expected = [["Name", "Country"]] + [ [r["name"], r["country"] + " different"] for r in self.AS_VALUES_DATA ] self.assertEqual(list(Table(self.AS_VALUES_DATA).as_values()), expected) def test_as_values_accessor_relation(self): programmer = Occupation.objects.create(name="Programmer") henk = Person.objects.create( first_name="Henk", last_name="Voornaman", occupation=programmer ) class Table(tables.Table): name = tables.Column(accessor=tables.A("first_name")) occupation = tables.Column( accessor=tables.A("occupation.name"), verbose_name="Occupation" ) expected = [["First name", "Occupation"], [henk.first_name, programmer.name]] self.assertEqual(list(Table(Person.objects.all()).as_values()), expected) class RowAttrsTest(SimpleTestCase): def test_row_attrs(self): class Table(tables.Table): alpha = tables.Column() beta = tables.Column() table = Table( MEMORY_DATA, row_attrs={"class": lambda table, record: "row-id-{}".format(record["i"])} ) self.assertEqual(table.rows[0].attrs, {"class": "row-id-2 even"}) def test_row_attrs_in_meta(self): class Table(tables.Table): alpha = tables.Column() beta = tables.Column() class Meta: row_attrs = {"class": lambda record: "row-id-{}".format(record["i"])} table = Table(MEMORY_DATA) self.assertEqual(table.rows[0].attrs, {"class": "row-id-2 even"}) def test_td_attrs_from_table(self): class Table(tables.Table): alpha = tables.Column() beta = tables.Column() class Meta: attrs = {"td": {"data-column-name": lambda bound_column: bound_column.name}} table = Table(MEMORY_DATA) html = table.as_html(request) td = parse(html).find(".//tbody/tr[1]/td[1]") self.assertEqual(td.attrib, {"data-column-name": "alpha"}) django-tables2-2.1.1/tests/test_templates.py0000644000175000017500000003436713542204315020436 0ustar josephjosephfrom django.template import Context, Template from django.test import SimpleTestCase, TestCase, override_settings from django.utils.translation import gettext_lazy, override as translation_override from lxml import etree import django_tables2 as tables from django_tables2.config import RequestConfig from .app.models import Person from .utils import build_request, parse class CountryTable(tables.Table): name = tables.Column() capital = tables.Column(orderable=False, verbose_name=gettext_lazy("Capital")) population = tables.Column(verbose_name="Population Size") currency = tables.Column(visible=False) tld = tables.Column(visible=False, verbose_name="Domain") calling_code = tables.Column(accessor="cc", verbose_name="Phone Ext.") MEMORY_DATA = [ { "name": "Germany", "capital": "Berlin", "population": 83, "currency": "Euro (€)", "tld": "de", "cc": 49, }, {"name": "France", "population": 64, "currency": "Euro (€)", "tld": "fr", "cc": 33}, {"name": "Netherlands", "capital": "Amsterdam", "cc": "31"}, {"name": "Austria", "cc": 43, "currency": "Euro (€)", "population": 8}, ] class TemplateTest(TestCase): @override_settings(DJANGO_TABLES2_TEMPLATE="foo/bar.html") def test_template_override_in_settings(self): class Table(tables.Table): column = tables.Column() table = Table({}) self.assertEqual(table.template_name, "foo/bar.html") def test_as_html(self): request = build_request("/") table = CountryTable(MEMORY_DATA) root = parse(table.as_html(request)) self.assertEqual(len(root.findall(".//thead/tr")), 1) self.assertEqual(len(root.findall(".//thead/tr/th")), 4) self.assertEqual(len(root.findall(".//tbody/tr")), 4) self.assertEqual(len(root.findall(".//tbody/tr/td")), 16) # no data with no empty_text table = CountryTable([]) root = parse(table.as_html(request)) self.assertEqual(1, len(root.findall(".//thead/tr"))) self.assertEqual(4, len(root.findall(".//thead/tr/th"))) self.assertEqual(0, len(root.findall(".//tbody/tr"))) # no data WITH empty_text table = CountryTable([], empty_text="this table is empty") root = parse(table.as_html(request)) self.assertEqual(1, len(root.findall(".//thead/tr"))) self.assertEqual(4, len(root.findall(".//thead/tr/th"))) self.assertEqual(1, len(root.findall(".//tbody/tr"))) self.assertEqual(1, len(root.findall(".//tbody/tr/td"))) self.assertEqual( int(root.find(".//tbody/tr/td").get("colspan")), len(root.findall(".//thead/tr/th")) ) self.assertEqual(root.find(".//tbody/tr/td").text, "this table is empty") # data without header table = CountryTable(MEMORY_DATA, show_header=False) root = parse(table.as_html(request)) self.assertEqual(len(root.findall(".//thead")), 0) self.assertEqual(len(root.findall(".//tbody/tr")), 4) self.assertEqual(len(root.findall(".//tbody/tr/td")), 16) # with custom template table = CountryTable([], template_name="django_tables2/table.html") table.as_html(request) def test_custom_rendering(self): """For good measure, render some actual templates.""" countries = CountryTable(MEMORY_DATA) context = Context({"countries": countries}) # automatic and manual column verbose names template = Template( "{% for column in countries.columns %}{{ column }}/" "{{ column.name }} {% endfor %}" ) result = "Name/name Capital/capital Population Size/population " "Phone Ext./calling_code " assert result == template.render(context) # row values template = Template( "{% for row in countries.rows %}{% for value in row %}" "{{ value }} {% endfor %}{% endfor %}" ) result = "Germany Berlin 83 49 France — 64 33 Netherlands Amsterdam " "— 31 Austria — 8 43 " assert result == template.render(context) class TestQueries(TestCase): def test_as_html_db_queries(self): class PersonTable(tables.Table): class Meta: model = Person request = build_request("/") with self.assertNumQueries(1): PersonTable(Person.objects.all()).as_html(request) def test_render_table_db_queries(self): """ Paginated tables should result in two queries: - one query for pagination: .count() - one query for records on the current page: .all()[start:end] """ Person.objects.create(first_name="brad", last_name="ayers") Person.objects.create(first_name="davina", last_name="adisusila") class PersonTable(tables.Table): class Meta: model = Person per_page = 1 request = build_request("/") with self.assertNumQueries(2): request = build_request("/") table = PersonTable(Person.objects.all()) RequestConfig(request).configure(table) html = Template("{% load django_tables2 %}{% render_table table %}").render( Context({"table": table, "request": request}) ) self.assertIn("brad", html) self.assertIn("ayers", html) class TemplateLocalizeTest(TestCase): simple_test_data = [{"name": 1234.5}] expected_results = {None: "1234.5", False: "1234.5", True: "1 234,5"} # non-breaking space def assert_cond_localized_table(self, localizeit=None, expected=None): """ helper function for defining Table class conditionally """ class TestTable(tables.Table): name = tables.Column(verbose_name="my column", localize=localizeit) self.assert_table_localization(TestTable, expected) def assert_table_localization(self, TestTable, expected): html = TestTable(self.simple_test_data).as_html(build_request()) self.assertIn("".format(self.expected_results[expected]), html) def test_localization_check(self): self.assert_cond_localized_table(None, None) # unlocalize self.assert_cond_localized_table(False, False) @override_settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True) def test_localization_different_locale(self): with translation_override("pl"): # with default polish locales and enabled thousand separator # 1234.5 is formatted as "1 234,5" with nbsp self.assert_cond_localized_table(True, True) # with localize = False there should be no formatting self.assert_cond_localized_table(False, False) # with localize = None and USE_L10N = True # there should be the same formatting as with localize = True self.assert_cond_localized_table(None, True) def test_localization_check_in_meta(self): class TableNoLocalize(tables.Table): name = tables.Column(verbose_name="my column") class Meta: default = "---" self.assert_table_localization(TableNoLocalize, None) @override_settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True) def test_localization_check_in_meta_different_locale(self): class TableNoLocalize(tables.Table): name = tables.Column(verbose_name="my column") class Meta: default = "---" class TableLocalize(tables.Table): name = tables.Column(verbose_name="my column") class Meta: default = "---" localize = ("name",) class TableUnlocalize(tables.Table): name = tables.Column(verbose_name="my column") class Meta: default = "---" unlocalize = ("name",) class TableLocalizePrecedence(tables.Table): name = tables.Column(verbose_name="my column") class Meta: default = "---" unlocalize = ("name",) localize = ("name",) with translation_override("pl"): # the same as in localization_check. # with localization and polish locale we get formatted output self.assert_table_localization(TableNoLocalize, True) # localize self.assert_table_localization(TableLocalize, True) # unlocalize self.assert_table_localization(TableUnlocalize, False) # test unlocalize has higher precedence self.assert_table_localization(TableLocalizePrecedence, False) def test_localization_of_pagination_strings(self): class Table(tables.Table): foo = tables.Column(verbose_name="my column") bar = tables.Column() class Meta: default = "---" table = Table(map(lambda x: [x, x + 100], range(40))) request = build_request("/?page=2") RequestConfig(request, paginate={"per_page": 10}).configure(table) with translation_override("en"): html = table.as_html(request) self.assertIn("previous", html) self.assertIn("next", html) with translation_override("nl"): html = table.as_html(request) self.assertIn("vorige", html) self.assertIn("volgende", html) with translation_override("fr"): html = table.as_html(request) self.assertIn("précédent", html) self.assertIn("suivant", html) class BootstrapTable(CountryTable): class Meta: template_name = "django_tables2/bootstrap.html" prefix = "bootstrap-" per_page = 2 class BootstrapTemplateTest(SimpleTestCase): def test_bootstrap_template(self): table = BootstrapTable(MEMORY_DATA) request = build_request("/") RequestConfig(request).configure(table) template = Template("{% load django_tables2 %}{% render_table table %}") html = template.render(Context({"request": request, "table": table})) root = parse(html) self.assertEqual(root.find(".//table").attrib, {"class": "table"}) self.assertEqual(len(root.findall(".//thead/tr")), 1) self.assertEqual(len(root.findall(".//thead/tr/th")), 4) self.assertEqual(len(root.findall(".//tbody/tr")), 2) self.assertEqual(len(root.findall(".//tbody/tr/td")), 8) self.assertEqual(root.find('.//ul[@class="pagination"]/li[2]/a').text.strip(), "2") # make sure the link is prefixed self.assertEqual( root.find('.//ul[@class="pagination"]/li[@class="next"]/a').get("href"), "?bootstrap-page=2", ) def test_bootstrap_responsive_template(self): class BootstrapResponsiveTable(BootstrapTable): class Meta(BootstrapTable.Meta): template_name = "django_tables2/bootstrap-responsive.html" table = BootstrapResponsiveTable(MEMORY_DATA) request = build_request("/") RequestConfig(request).configure(table) template = Template("{% load django_tables2 %}{% render_table table %}") html = template.render(Context({"request": request, "table": table})) root = parse(html) self.assertEqual(len(root.findall(".//thead/tr")), 1) self.assertEqual(len(root.findall(".//thead/tr/th")), 4) self.assertEqual(len(root.findall(".//tbody/tr")), 2) self.assertEqual(len(root.findall(".//tbody/tr/td")), 8) self.assertEqual(root.find('.//ul[@class="pagination"]/li[2]/a').text.strip(), "2") class SemanticTemplateTest(SimpleTestCase): def test_semantic_template(self): class SemanticTable(CountryTable): class Meta: template_name = "django_tables2/semantic.html" prefix = "semantic-" per_page = 2 table = SemanticTable(MEMORY_DATA) request = build_request("/") RequestConfig(request).configure(table) template = Template("{% load django_tables2 %}{% render_table table %}") html = template.render(Context({"request": request, "table": table})) root = parse(html) self.assertEqual(len(root.findall(".//thead/tr")), 1) self.assertEqual(len(root.findall(".//thead/tr/th")), 4) self.assertEqual(len(root.findall(".//tbody/tr")), 2) self.assertEqual(len(root.findall(".//tbody/tr/td")), 8) # make sure the link is prefixed next_page = './/tfoot/tr/th/div[@class="ui right floated pagination menu"]/a[1]' self.assertEqual(root.find(next_page).get("href"), "?semantic-page=1") class ValidHTMLTest(SimpleTestCase): template = """Basic html template to render a table {% load django_tables2 %}{% render_table table %} """ allowed_errors = {etree.ErrorTypes.HTML_UNKNOWN_TAG: ["Tag nav invalid"]} context_lines = 4 def test_templates(self): parser = etree.HTMLParser() for name in ("table", "semantic", "bootstrap", "bootstrap4"): table = CountryTable( list([MEMORY_DATA] * 10), template_name="django_tables2/{}.html".format(name) ).paginate(per_page=5) html = Template(self.template).render( Context({"request": build_request(), "table": table}) ) # will raise lxml.etree.XMLSyntaxError if markup is incorrect etree.fromstring(html, parser) for error in parser.error_log: if ( error.type in self.allowed_errors and error.message in self.allowed_errors[error.type] ): continue lines = html.splitlines() start, end = ( max(0, error.line - self.context_lines), min(error.line + self.context_lines, len(lines)), ) context = "\n".join( [ "{}: {}".format(i, line) for i, line in zip(range(start + 1, end + 1), lines[start:end]) ] ) raise AssertionError( "template: {}; {} \n {}".format(table.template_name, str(error), context) ) django-tables2-2.1.1/tests/test_utils.py0000644000175000017500000001620513542204315017567 0ustar josephjosephfrom unittest import TestCase from django.db import models from django_tables2.utils import ( Accessor, AttributeDict, OrderBy, OrderByTuple, Sequence, call_with_appropriate, computed_values, segment, signature, ) class OrderByTupleTest(TestCase): def test_basic(self): obt = OrderByTuple(("a", "b", "c")) assert obt == (OrderBy("a"), OrderBy("b"), OrderBy("c")) def test_intexing(self): obt = OrderByTuple(("a", "b", "c")) assert obt[0] == OrderBy("a") assert obt["b"] == OrderBy("b") with self.assertRaises(KeyError): obt["d"] with self.assertRaises(TypeError): obt[("tuple",)] def test_get(self): obt = OrderByTuple(("a", "b", "c")) sentinel = object() assert obt.get("b", sentinel) is obt["b"] # keying assert obt.get("-", sentinel) is sentinel assert obt.get(0, sentinel) is obt["a"] # indexing assert obt.get(3, sentinel) is sentinel def test_opposite(self): assert OrderByTuple(("a", "-b", "c")).opposite == ("-a", "b", "-c") def test_in(self): obt = OrderByTuple(("a", "b", "c")) assert "a" in obt and "-a" in obt def test_sort_key_multiple(self): obt = OrderByTuple(("a", "-b")) items = [{"a": 1, "b": 2}, {"a": 1, "b": 3}] assert sorted(items, key=obt.key) == [{"a": 1, "b": 3}, {"a": 1, "b": 2}] def test_sort_key_empty_comes_first(self): obt = OrderByTuple(("a")) items = [{"a": 1}, {"a": ""}, {"a": 2}] assert sorted(items, key=obt.key) == [{"a": ""}, {"a": 1}, {"a": 2}] class OrderByTest(TestCase): def test_orderby_ascending(self): a = OrderBy("a") self.assertEqual(a, "a") self.assertEqual(a.bare, "a") self.assertEqual(a.opposite, "-a") self.assertTrue(a.is_ascending) self.assertFalse(a.is_descending) def test_orderby_descending(self): b = OrderBy("-b") self.assertEqual(b, "-b") self.assertEqual(b.bare, "b") self.assertEqual(b.opposite, "b") self.assertTrue(b.is_descending) self.assertFalse(b.is_ascending) class AccessorTest(TestCase): def test_bare(self): self.assertEqual(Accessor("").resolve("Brad"), "Brad") self.assertEqual(Accessor("").resolve({"Brad"}), {"Brad"}) self.assertEqual(Accessor("").resolve({"Brad": "author"}), {"Brad": "author"}) def test_index_lookup(self): self.assertEqual(Accessor("0").resolve("Brad"), "B") self.assertEqual(Accessor("1").resolve("Brad"), "r") self.assertEqual(Accessor("-1").resolve("Brad"), "d") self.assertEqual(Accessor("-2").resolve("Brad"), "a") def test_calling_methods(self): self.assertEqual(Accessor("2.upper").resolve("Brad"), "A") self.assertEqual(Accessor("2.upper.__len__").resolve("Brad"), 1) def test_honors_alters_data(self): class Foo: deleted = False def delete(self): self.deleted = True delete.alters_data = True foo = Foo() with self.assertRaises(ValueError): Accessor("delete").resolve(foo) self.assertFalse(foo.deleted) def test_accessor_can_be_quiet(self): self.assertIsNone(Accessor("bar").resolve({}, quiet=True)) def test_penultimate(self): context = {"a": {"a": 1, "b": {"c": 2, "d": 4}}} self.assertEqual(Accessor("a.b.c").penultimate(context), (context["a"]["b"], "c")) self.assertEqual(Accessor("a.b.c.d.e").penultimate(context), (None, "e")) class AccessorTestModel(models.Model): foo = models.CharField(max_length=20) class Meta: app_label = "tests" class AccessorModelTests(TestCase): def test_can_return_field(self): context = AccessorTestModel(foo="bar") self.assertIsInstance(Accessor("foo").get_field(context), models.CharField) def test_returns_None_when_doesnt_exist(self): context = AccessorTestModel(foo="bar") self.assertIsNone(Accessor("bar").get_field(context)) def test_returns_None_if_not_a_model(self): context = {"bar": 234} self.assertIsNone(Accessor("bar").get_field(context)) class AttributeDictTest(TestCase): def test_handles_escaping(self): # django==3.0 replaces ' with ', drop first option if django==2.2 support is removed self.assertIn( AttributeDict({"x": "\"'x&"}).as_html(), ('x=""'x&"', 'x=""'x&"'), ) def test_omits_None(self): self.assertEqual(AttributeDict({"x": None}).as_html(), "") def test_self_wrap(self): x = AttributeDict({"x": "y"}) self.assertEqual(x, AttributeDict(x)) class ComputedValuesTest(TestCase): def test_supports_shallow_structures(self): x = computed_values({"foo": lambda: "bar"}) self.assertEqual(x, {"foo": "bar"}) def test_supports_nested_structures(self): x = computed_values({"foo": lambda: {"bar": lambda: "baz"}}) self.assertEqual(x, {"foo": {"bar": "baz"}}) def test_with_argument(self): x = computed_values( {"foo": lambda y: {"bar": lambda y: "baz-{}".format(y)}}, kwargs=dict(y=2) ) self.assertEqual(x, {"foo": {"bar": "baz-2"}}) def test_returns_None_if_not_enough_kwargs(self): x = computed_values({"foo": lambda x: "bar"}) self.assertEqual(x, {"foo": None}) class SegmentTest(TestCase): def test_should_return_all_candidates(self): assert set(segment(("a", "-b", "c"), {"x": "a", "y": ("b", "-c"), "-z": ("b", "-c")})) == { ("x", "-y"), ("x", "z"), } class SequenceTest(TestCase): def test_multiple_ellipsis(self): sequence = Sequence(["foo", "...", "bar", "..."]) with self.assertRaises(ValueError): sequence.expand(["foo"]) class SignatureTest(TestCase): def test_basic(self): def foo(bar, baz): pass args, keywords = signature(foo) assert args == ("bar", "baz") assert keywords is None def test_signature_method(self): class Foo: def foo(self): pass def bar(self, bar, baz): pass def baz(self, bar, *bla, **boo): pass obj = Foo() args, keywords = signature(obj.foo) assert args == () assert keywords is None args, keywords = signature(obj.bar) assert args == ("bar", "baz") assert keywords is None args, keywords = signature(obj.baz) assert args == ("bar",) assert keywords == "boo" def test_catch_all_kwargs(self): def foo(bar, baz, **kwargs): pass args, keywords = signature(foo) assert args == ("bar", "baz") assert keywords == "kwargs" class CallWithAppropriateTest(TestCase): def test_basic(self): def foo(): return "bar" assert call_with_appropriate(foo, {"a": "d", "c": "e"}) == "bar" def bar(baz): return baz assert call_with_appropriate(bar, dict(baz=23)) == 23 django-tables2-2.1.1/tests/test_faq.py0000644000175000017500000000247413542204315017201 0ustar josephjosephfrom django.test import SimpleTestCase import django_tables2 as tables from .utils import build_request, parse TEST_DATA = [ {"name": "Belgium", "population": 11200000}, {"name": "Luxembourgh", "population": 540000}, {"name": "France", "population": 66000000}, ] class FaqTest(SimpleTestCase): def test_row_counter_using_templateColumn(self): class CountryTable(tables.Table): counter = tables.TemplateColumn("{{ row_counter }}") name = tables.Column() expected = "" table = CountryTable(TEST_DATA) html = table.as_html(build_request()) self.assertIn(expected, html) # the counter should start at zero the second time too table = CountryTable(TEST_DATA) html = table.as_html(build_request()) self.assertIn(expected, html) def test_row_footer_total(self): class CountryTable(tables.Table): name = tables.Column() population = tables.Column( footer=lambda table: "Total: {}".format(sum(x["population"] for x in table.data)) ) table = CountryTable(TEST_DATA) html = table.as_html(build_request()) columns = parse(html).findall(".//tfoot/tr")[-1].findall("td") self.assertEqual(columns[1].text, "Total: 77740000") django-tables2-2.1.1/tests/test_config.py0000644000175000017500000000662013542204315017674 0ustar josephjosephfrom django.core.paginator import EmptyPage, PageNotAnInteger from django.test import SimpleTestCase, TestCase from fudge import Fake from django_tables2 import Column, RequestConfig, Table from .app.models import Person from .utils import build_request NOTSET = object() # unique value class ConfigTest(SimpleTestCase): def table(self): return Fake("Table").has_attr( prefixed_page_field="page", prefixed_per_page_field="per_page", prefixed_order_by_field="sort", ) def test_no_querystring(self): table = self.table().has_attr(order_by=NOTSET).expects("paginate") RequestConfig(build_request("/")).configure(table) self.assertEqual(table.order_by, NOTSET) def test_full_querystring(self): table = self.table() request = build_request("/?page=1&per_page=5&sort=abc") table = ( table.expects("paginate") .with_args(page=1, per_page=5) .expects("order_by") .with_args("abc") ) RequestConfig(request).configure(table) def test_partial_querystring(self): table = self.table() request = build_request("/?page=1&sort=abc") table = ( table.expects("paginate") .with_args(page=1, per_page=5) .expects("order_by") .with_args("abc") ) RequestConfig(request, paginate={"per_page": 5}).configure(table) def test_silent_page_not_an_integer_error(self): table = self.table() request = build_request("/") paginator = Fake("Paginator").expects("page").with_args(1) table = ( table.has_attr(paginator=paginator) .expects("paginate") .with_args(page="abc") .raises(PageNotAnInteger) ) RequestConfig(request, paginate={"page": "abc", "silent": True}).configure(table) def test_silent_empty_page_error(self): table = self.table() request = build_request("/") paginator = Fake("Paginator").has_attr(num_pages=987).expects("page").with_args(987) table = ( table.has_attr(paginator=paginator) .expects("paginate") .with_args(page=123) .raises(EmptyPage) ) RequestConfig(request, paginate={"page": 123, "silent": True}).configure(table) def test_passing_request_to_constructor(self): """Table constructor should call RequestConfig if a request is passed.""" request = build_request("/?page=1&sort=abc") class SimpleTable(Table): abc = Column() table = SimpleTable([{}], request=request) self.assertTrue(table.columns["abc"].is_ordered) class NoPaginationQueriesTest(TestCase): def test_should_not_count_with_paginate_False(self): """ No extra queries with pagination turned off. https://github.com/jieter/django-tables2/issues/551 """ class MyTable(Table): first_name = Column() class Meta: template_name = "minimal.html" request = build_request() Person.objects.create(first_name="Talip", last_name="Molenschot") table = MyTable(Person.objects.all()) RequestConfig(request, paginate=False).configure(table) with self.assertNumQueries(1): html = table.as_html(request) self.assertIn("
{0} 0
", html) django-tables2-2.1.1/pyproject.toml0000644000175000017500000000003713542204315016564 0ustar josephjoseph[tool.black] line-length = 100 django-tables2-2.1.1/.coveragerc0000644000175000017500000000013713542204315015772 0ustar josephjoseph[run] source = django_tables2 tests branch = true [html] directory = reports/htmlcov django-tables2-2.1.1/tox.ini0000644000175000017500000000317313542204315015167 0ustar josephjoseph[tox] args_are_paths = false envlist = py35-{2.1,2.2}, py36-{2.1,2.2,3.0,master}, py37-{2.1,2.2,3.0,master}, py38-{3.0,master}, docs, flake8, isort, black [travis] python: 3.5: py35 3.6: py36, docs, flake8, isort 3.7-dev: py37 [testenv] basepython = py35: python3.5 py36: python3.6 py37: python3.7 py38: python3.8 usedevelop = true pip_pre = true setenv = PYTHONPATH={toxinidir} PYTHONWARNINGS=all commands = coverage run --source=django_tables2 manage.py test {posargs} deps = 2.1: Django==2.1.* 2.2: Django==2.2.* 3.0: Django==3.0a1 master: https://github.com/django/django/archive/master.tar.gz coverage -r{toxinidir}/requirements/common.pip [testenv:docs] basepython = python3.6 whitelist_externals = make changedir = docs setenv = PYTHONWARNINGS=default commands = make html make spelling deps = -r{toxinidir}/docs/requirements.txt [testenv:flake8] basepython = python3.6 deps = flake8==3.7.8 commands = flake8 [flake8] ignore = E731,W503,E203 exclude = .git,__pycache__,.tox,example/app/migrations max-line-length = 120 [testenv:black] basepython = python3.6 passenv = LC_CTYPE deps = black==19.3b0 commands = black --check . [testenv:isort] basepython = python3.6 deps = isort==4.3.21 commands = isort --diff --check --recursive {toxinidir}/django_tables2 {toxinidir}/tests {toxinidir}/example [isort] multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 combine_as_imports = True line_length = 100 skip = migrations known_third_party=django,django_filters,pytest,fudge,lxml,pytz known_first_party=django_tables2 django-tables2-2.1.1/LICENSE0000644000175000017500000000316713542204315014664 0ustar josephjosephAll changes made to django-tables2 since forking from django-tables are Copyright (c) 2011, Bradley Ayers All rights reserved. Redistribution is permitted under the same terms as the original django-tables license. The original django-tables license is included below. Copyright (c) 2008, Michael Elsdörfer All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-tables2-2.1.1/CONTRIBUTING.md0000644000175000017500000000336313542204315016106 0ustar josephjoseph# Contributing to django-tables2 You are welcome to contribute to the development of `django-tables2` in various ways: - Discover and [report bugs](https://github.com/jieter/django-tables2/issues/new). Make sure to include a minimal example to show your problem. - Propose features, add tests or fix bugs by [opening a Pull Request](https://github.com/jieter/django-tables2/compare) - Fix documentation or translations When contributing features or making bug fixes, please add unit tests to verify the expected behaviour. This helps ## Coding style We use [black](https://black.readthedocs.io/en/stable/) to format the sources, with a 100 char line length. Before committing, run `black .`, or use `pre-commit`: ``` pip install pre-commit pre-commit install ``` ## Running the tests With `tox` installed, you can run the test suite in all supported environments by typing `tox`. During development, you might not want to wait for the tests to run in all environments, in that case, use the `-e` argument to specify a specific environment. For example `tox -e py36-2.0` will run the tests in python 3.6 with Django 2.0. You can also run the tests only in your current environment, using `PYTHONPATH=. ./manage.py test` (which is even quicker). ## Code coverage To generate a html coverage report: ``` coverage run --source=django_tables2 manage.py test coverage html ``` ## Building the documentation If you want to build the docs from within a virtualenv, and Sphinx is installed globally, use: ``` cd docs/ make html SPHINXBUILD="python $(which sphinx-build)" ``` Publishing a release -------------------- 1. Bump the version in `django-tables2/__init__.py`. 2. Update `CHANGELOG.md`. 3. Create a tag `./maintenance.py tag`. 4. Run `./maintenance.py publish` django-tables2-2.1.1/setup.py0000755000175000017500000000323013542204315015363 0ustar josephjoseph#!/usr/bin/env python3 import re from setuptools import find_packages, setup # get version without importing with open("django_tables2/__init__.py", "rb") as f: VERSION = str(re.search('__version__ = "(.+?)"', f.read().decode()).group(1)) setup( name="django-tables2", version=VERSION, description="Table/data-grid framework for Django", long_description=open("README.md").read(), long_description_content_type="text/markdown", author="Bradley Ayers", author_email="bradley.ayers@gmail.com", license="Simplified BSD", url="https://github.com/jieter/django-tables2/", packages=find_packages(exclude=["tests.*", "tests", "example.*", "example", "docs"]), include_package_data=True, # declarations in MANIFEST.in install_requires=["Django>=1.11"], extras_require={"tablib": ["tablib"]}, classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Framework :: Django :: 1.11", "Framework :: Django :: 2.1", "Framework :: Django :: 2.2", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries", ], ) django-tables2-2.1.1/maintenance.py0000755000175000017500000000365513542204315016520 0ustar josephjoseph#!/usr/bin/env python3 import os import re import subprocess import sys import time # get version without importing with open("django_tables2/__init__.py", "rb") as f: VERSION = str(re.search('__version__ = "(.+?)"', f.read().decode()).group(1)) if sys.argv[-1] == "publish": os.system("python setup.py clean --all") os.system("python setup.py sdist bdist_wheel --universal") os.system( "twine upload dist/django-tables2-{version}.tar.gz" " dist/django_tables2-{version}-py2.py3-none-any.whl".format(version=VERSION) ) print( "\nreleased [{version}](https://pypi.org/project/django-tables2/{version}/)".format( version=VERSION ) ) sys.exit() if sys.argv[-1] == "tag": os.system("git tag -a v{} -m 'tagging v{}'".format(VERSION, VERSION)) os.system("git push --tags && git push origin master") sys.exit() if sys.argv[-1] == "screenshots": def screenshot(url, filename="screenshot.png", delay=2): print("Making screenshot of url: {}".format(url)) chrome = subprocess.Popen( ["chromium-browser", "--incognito", "--headless", "--screenshot", url], close_fds=False ) print("Starting to sleep for {}s...".format(delay)) time.sleep(delay) chrome.kill() os.system("convert screenshot.png -trim -bordercolor White -border 10x10 {}".format(dest)) os.remove("screenshot.png") print("Saved file to", dest) images = { "{url}/tutorial/": "docs/img/example.png", "{url}/bootstrap/": "docs/img/bootstrap.png", "{url}/bootstrap4/": "docs/img/bootstrap4.png", "{url}/semantic/": "docs/img/semantic.png", } print( "Make sure the devserver is running: \n cd example/\n PYTHONPATH=.. ./manage.py runserver --insecure\n\n" ) for url, dest in images.items(): screenshot(url.format(url="http://localhost:8000"), dest) sys.exit() django-tables2-2.1.1/manage.py0000755000175000017500000000037513542204315015462 0ustar josephjoseph#!/usr/bin/env python3 import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.app.settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) django-tables2-2.1.1/MANIFEST.in0000644000175000017500000000044713542204315015413 0ustar josephjosephinclude README.md include CHANGELOG.md include LICENSE recursive-exclude docs/_build * recursive-include django_tables2/templates * recursive-include django_tables2/static * recursive-include django_tables2/locale * recursive-include example/app/fixtures * recursive-include example/templates * django-tables2-2.1.1/docs/0000755000175000017500000000000013542204315014600 5ustar josephjosephdjango-tables2-2.1.1/docs/index.rst0000644000175000017500000000337313542204315016447 0ustar josephjoseph.. default-domain:: py ================================================ django-tables2 - An app for creating HTML tables ================================================ Its features include: - Any iterable can be a data-source, but special support for Django QuerySets is included. - The built in UI does not rely on JavaScript. - Support for automatic table generation based on a Django model. - Supports custom column functionality via subclassing. - Pagination. - Column based table sorting. - Template tag to enable trivial rendering to HTML. - Generic view mixin. About the app: - `Available on pypi `_ - Tested against currently supported versions of Django `and the python versions Django supports `_ (see `Travis CI `_) - `Documentation on readthedocs.org `_ - `Bug tracker `_ Table of contents ----------------- .. toctree:: :maxdepth: 1 :caption: Getting started pages/installation pages/tutorial pages/table-data .. toctree:: :maxdepth: 1 :caption: Customization pages/custom-data pages/ordering pages/column-attributes pages/column-headers-and-footers pages/swapping-columns pages/pagination pages/table-mixins pages/custom-rendering pages/query-string-fields pages/localization-control pages/generic-mixins pages/pinned-rows pages/filtering pages/export .. toctree:: :maxdepth: 1 :caption: Reference pages/reference pages/faq pages/upgrade-changelog pages/glossary django-tables2-2.1.1/docs/img/0000755000175000017500000000000013542204315015354 5ustar josephjosephdjango-tables2-2.1.1/docs/img/example.png0000644000175000017500000012231713542204315017523 0ustar josephjosephPNG  IHDRgzJgAMA a cHRMz&u0`:pQ<bKGD oFFs35tIME ;*[h vpAg4lqIDATxwT「(UEE"AرKD5j,X&{%{Wޱ]/Ao9#<3;;B$I$I$IO%I$I@,I$I$bI$I 9K$IɁX$I4HĒ$IAr $I$ $I$iP%K`hhJDݻ###7o.8t۷/* JŴi-gR~}/N:u*#F@__ٳgg.>>VZ)]dI?uT%ݻ:{cƌQkѢEؿ?|2t|C7o1$$D!g} qݼryz/"ӶzSTTvSN͗]& |;[:v(CBBx/^^͝;W޽;2nݺ}:t͛7vǏ ޽{[v}G9}ǰrJ>7I?wikkk:>>믹|2},00޽{k9W^aÆ+W+`ȑ9zPP~~$}-Z J?x 5/_T=zCҾ}{*VHr:uTVMv˖-ShQ\]]qFoߎ7GNRĉI&uW.O>AݻH466f9כ֯_OJӣyļ38G@@|W<\Yo>UT*P4j(C^-[L믿(V* Z͖-[8z(J >cccQըT*6lOйsg^z@R066ŋLLL믿+++vޝe{9su @֭166f׮]o]`ʕdɒ̜9fח͛Ӹqc²|7bmmӳL{u6mJvpww_UEժU믿}6ƄO]x[[[e_Ʋ{nJ,JB ֡O>ʅc066nݺo{WϮڵ)Sݻ7%KD1/^0tPLMM)Q'NT={vgf͢t3jԨm6ڷoOF000?Ża׮]Ԯ].]榴Cv&"""yz\xVZahhҥKtޝmbbbB*Uؼy>""%7ҨQ#-JYf i]]]ʗ/lG=mNJ^קbŊ9>?OHs挲Z,^Xԭ[WXُپ}ψӟ'!C(iϞ=>|,%%E KKKe]vh׮HIIgV~[hѢBUV @̙3GIӮ]; \׏'O!hԨ[e֧]~NҥKʶҥKb…2OOOqytReٙ3gӟ,YR/bXoUҭ_g+VXlY>X\ }0vزe(T矅B|BKKK?>˶z6TbŊ"))I 4H)y\zU*ϬyBĥKtRFnݺ @vډ=z(ʔ?J%vءӯ_OHcжm[qww ̞^rK.qM8r7v1%;ͩVo?kkk)B"E!55SP!BCC3gxxxi1ccc|1zcXZZ2d w˖-Yb =':::)Sccc֮]˭[ ɓ'ٳ%K`cc9]RRwgo۷/W\ ]-ZիWi&:wlϔOС>Txߗƍ_~ʲӧOioΜ9CCCP8::κu2ڵk,[ ݻVׯe˖}+رcy~~~@#~!ۘsڧ2k>?;w2w\/V^+V ..m۶m|gVXq㈋Yf]M6pΝ }6$2ȯ|'',/٣c!}^SN&O}Lm7ח.\XY# C_^ ;qu4ѣGSX1ڵkǣG8t萲]xe"""?~<ŋʀm O۷5k@;r%PTlݺqg۞9s RRR26Rr<߻wix j5'N$88FѴiStuuyE?k;7]{{9YT`uFRRk֬ŋ>|8߿ϨQXt)nnn9_Nسg3zQhQ֮]ӧO=zt><˗+VXHMMԮ];W͏ccxˉw=}ѣGYsqqa޼yL>=]sǗ_~hkk3fz+W2h e1sexzzfmi&''S mxjH9.. ӏ/?~ g֭[TV SR\ rrMڵ}:} 炤$4iBݙ={[/[~Hm勜6Rr|WTI x<}/'WiOf̘1!\%&&ұcGƌêUX`cƌQP~~1(n䲚m^K*R-[r>ϟ?_~Qٳ+Wp=~g\]]9u +iӦqm9s&ӧOsgfiڴ)ZBGG---ڷoo3y3] o$߽{Wy6m|}v+lsڧ^qfn>*_~Ho@GGGyruԩaefQ.~w弑}I~,GGG3|eٳٶmJ΂m͚56Rr<(S'ׯg̟?i/Xdv~Ӯ];*֭[>|x"I:KuׯDGGgQѣ|P5jTw)\0JN{a˖-9Wϟ+H4iKPիr:fرcٺu+vvv1ydBBB(^2H6k ݻw3vXΟ?7|7|È#Xr"\vҧ^"Ka&Ū׽ʗ/|!c*KAf^ԩS(fg5WLƍ:u*}p:777|||9s&gΜaС+Wґ#G*w;޽{!}*/GvZߟmfO"""8wkuΪ^Wz2;߼#|ٳg/^P^8p2Y/_ LةS'/^ٵkh*vQjԬY)SЦMso6?3M4᫯ HICqGGGӬY3l{\_mpB¸z*AAA@ԾOrr2Bj)y:99Y9R%J^Wxxx~%K(iǏǗ={&jժ%+&ڴi#EbD*UĥKč7DՕm+q)u@B!~駷H"B bȑkIIIbر^ʕ+~^3ߵkŋ|HMMQb6l[NNNb…J/^PNUVNB} ^zXbL :T$&& !>---QV-aggSdd044|+HҦU&bcc3,?Z(^شicNDbń8s挈۷Wb]xQcǎ~hƌdɒ\Zꭾann.N>|...ƍB!%DLJtuuEٲeŌ3D͕/^mhѢ" @TN/11Q W^;v̰ŋ0!G "9rѶ7nڵk+۷iFĈK QbE1w\#5jR!55ULg֭[cZ[[dɒɛϽj*IC|SyOf@<xxx( ΐvQn]&LKt.oҥX[[+?WpqqXb0bbcc5kŊcܼyq)ir*ۛ .HݺuiҤ BLÇy8;;Ӷm[ *Ċ+8|0DGGS\]]f<|5kҾ}{n޼I`` oFVcggGǎsloIs2QF;v 777^xÇիK&11m۶q >}1)3BVVVj"##133/Ԕ֮]ٳg)Z(YƗ~'ޭ[7j֬Itt4'NB 5*>IPPZZZ9SSS6mʃ|27-Z*UЪU++ԭo߾TV@oN<Hhh(SL?`ĉ]58q?ĄzjׯO֭5چrXҼ,Sܸq#2tR<==COOOOO.]=ԩS;v,cǎe߾}Ɵ3+GVOrr2[lɓ'!}CCCϱc2l@Zȑ#8q5kRP!OTTFVW_ѤIN:ņ >Z{׾~ J(Aɒ%ׯgǎ0d)ŋG\\pmv܉u5BWW7cVZ9r!ԩSG/6*W&&&r]]]*T@LL gÇo'''>su^lPj5cٲet5Oqummm?X000ܸq'OPD lll8u?ѣGTdzfjbݺu,]X4h@B4~Hn߾ͽ{xݻi;T~NyyDNϳŋɉ3g(vsJ><>ʕ+!!!e:pBN8'Optt>ǝܜwydɒƲm6LMM]J9_?,Y2GGGl‰'044ؘ۷oz[Ҝ٭K_VzuX~=$$$pi5k`.bŊi&3 g;DDDRUP 3puuESɉ@)U$''ZV>LBB7cvL+=vf]9s ꜛ_m(LpVZj*> ocn۶ ---LMMi޼9~~~Y/'׿iӦ\*Vϟ?v뼼|2ϟ޽{.brr簾ߟ۷~^|I%ru"ˏ4'7]۶m100ԩS\… S\9 )>e˖q”̪OPzuBBBR oѹsgʔ)9rE [[[ӧ[neڵ-Z///Zh_r0GoU;q<ȉ':ujbddģG]vF~3*V \rܾ}.I_ӧO'22cggQ/;v,*ɓ'ShQMI}Kʧ .I_7o$22SSS*U:vYr ODHL0cGJ*jԤʥwORQ>I65 9{g8::ɂ 4RUV 6iƍ9ʹL0 &|xcccQT<|H4A8wZwȨD< tBrr2ӧO 333*T@߾}C |-['׮]t}E9wPpSNѺuk oO?į-QIII; J6]Fboh@MY ׏={Ľ{8y$ @ܵkWΟ?_ྉ&99}RR%#11RN qttqƙRӧuޞ5k(OZpqqÃcN/^P._<:u ""f͚JժUYx1 4KKK CpqqΎJ*rJٻw/W&$$>}֭[ kkk*U 4iƍSs9jժ͛7ٳg 63g* ƴi3g3ϟϴ .𻎎N_RN_Сjբjժէp!:+˦{@Rm(Dtzׯ_gժUZRJ`llȑ#qww'22,--X"z"%%H۷חʕ+3l0/XB)'Ƅ h߾=CWW`\vvvp%-[䄝 6֭[J>ԭ[ׯ5ݼ.C7o|g[[[q̙fd{``R~?|HLLW>|Xg? ?."44T.]Z!DLLpqq=Bq bG"99Y7 !OB3aii)ŷ~+"99Y`?eGG<&1)dHJI-6~i:qڵ5Ž{iiڵŖ-[i̙3iǶ BqUann.1~x..\ RRRDRR\شiB 333c)J*%„i@:u|Zj%B|gbܹn>闵r }ݝFQpʫFCJJ #::VZ)^|Ijj*[neӦM*TVZٳg믕ܺu F5jаaC\]]9r$N;zhN8AJJ /_$&&ŋs;OOOʖ- @.];wnsԭ[???>,--קbŊW`yɂKX{m =*q_Dz%WoMY lNN…Yd w&..pS__###Utٝ76lŽ{hٲ%UTΎcǎL{#gϞߟ%m-ot~>֖PvIPPÆ {kTR*O:m_Ajj*nnnfͼSSS0… :u*Ǐg޼y9r  D >}z4i҄&MgtttSN.orrr T*D:---N:Ů]8v̟?&MzeUFVi_?aƣ'.-`ՕXXQʓ !iҟ˷ߩJ*LLLZ?zh^ʪU011O>Yݻc8xj|!R>/8ut҅ٳgcddDLLL4DGGRW^n:իٳIMMEݻ)[,L:H;`vŃرcJ⫯bΜ9\r!6lӓ#F0l0._VDDD(y>xf͚CTTY}S3/_`(SRv|| yL2ƍs_ ɓ'SGYX2fZr{mV-eӨP\~]iԨ۷W`M`t(eN]2*hR#4]28ږҌ/{tŋH_Mrr2Occ1WCMWEȗ?'C|3M3F|?G%W邏9?7i 789j:&{1N8T2{C{Bh֝x6DrJ(;ٜm JޓD5] _"_@Mܻz{]U*VCsV:mٹ}+m[6:еckצn7e`ƴx +ƣ=Ǐ) yfŲxUa ܹIjpv¥5}AJJJi̝M]w|ӾM߿i=0*vqsNMgگ7W<\Vْŋ nζTw׮'mksn->^չt<P*"(~2MҲi=yҺy?i]9Hӆ>n9ҥC+Ư++-mF4I2m0ڵgG+̞P۳i۲#o)ez0૞X[ˬaD5Mgx)׮]Y#_ч<Grr2v3|H^vf%LxMtVYӁRoM/Y4i^9EbN_ɚ [vZZylu1|H̜/^<[9~{ǷXU>ܵ'G_` #3W y=gv׎m bǞ8zߺ̜]1/g/sU&M1g߬Z{rR8'MCR1廱$&&|2iڼUm46 ҕ/osq.4gQý&s~YٮA V®}(QWeZB0?ؾ Xjc LJimu܆-ru=Qwn3d`oӗiҬ%zwЇZ+7U!.c[ؽ?sr1{B>ܖ_ $ef/w 3.Q$G_dڿ35J˜2H+oAܾ5LM042ϿzwG!͎[b) JRpaVX{x<Ȉpbb2M}f.^8Gxr,2M}&7׮ݘhvmF.Sͣf[Yr# Ib]==[XҷWSrp ƌJLLOSUΡrttt%Ν<ҥ13+E ܻ~Vy Ku1QܹIÿ^C֮^7,ܵ'# !!Jm!/6.\ߺYO=TmSɎS'_!i}UgQ!_݆n5YpikG4@(6X*TkVBl|>kՈSo90m}iMa71lEbrulK_ 3Mbl{2M]TLe6 h輕˗ pU-Z CU7Ӧ/ope~uYjqϞpyݻo=TطRpe;~,;a2u7<|Jʬ+soެ޵G2a*^z#UcG+-ĵŊ2g~_p. 7yٴ.BB/]P#C{wԴ=+9HgPkN`i9[qSjNBS#`;8Rz >e2crJhw*23tP4jJF;ؖ|G|;kV.T29*\Z>_ʕ٭^QH&0u勏WuJ(ID¿v =zs<{[y8_te)^@99Y6ZgG+|։o.l{:EiliGhڴhHrr io([<CC#ʘUֵh7o\Oڀ`llʂk;a #=W!G3_/-YrWR5c _==}l֣7jZO}?OtuCW3&oֵs}-YC3cڤg=]?AϚy>wI޹=sZ@ΟRXb=Ť#)ZsхkDDBK0:::̙,wӯscBB`-0!\j---\ʼn+Ӧnjw%6,9yCԝ$I#$I$ $I$i%I$I@,I$ITI%)7d_(_sɓ'O l쒔َR^ɩiI$I 9K$IɁX$I4HĒ$IAr $I$ 5-I hԨ*㍛;/ IȁX>!1 M*QJ*_d?ZJk:DI$_KΎ@MKbϞ=CO{;TR)fH 0 ˗ӱcGlB\\\yxj޼9ΝtUU˖-sL4Çk:!_-[۷oz嘯/GTT/^o߾TT+Waaa4nkkk,--5kϟ?GOOOceeDFFbcc%+VW^iW۷חʕ+3l0/XB)'i֭4i$ 6ƍo,\GGGƏ8pJ*ѪU+jժ3.\PcggG>}x%&LߟuAyCT3uVMZ۠Q>0 +V0x`\]]ٴi̗_~ԩSX~=۶mԩS MlWD nJHH%KdժU5ju͙>N1Ё%x*VI#[PPvoߞ˗gXY~sN.\ɓ'yrywG^չ>w9y$o>xlقʕoF!Y~DDf"..777/NHH[SߧSN,^ |M˷#Ã'O0sL|||HNNtrĉj www֮]\%mݺSP!j5Z"**Hwre;;;\B߾}Yd 666.\… dիW\p>ժU2GWrY)S ܾ};vйsg+ZgϞرCٮJ*ʕIgDb5ؒ~ņ4^e5f|rڵk]9r;w(ߧooٲ?cccr˻#\Y[lgϞ(Mrn߾͝;wXp!K.}g 6ZMBeҴiSBBtr!jԨ oƚ]ܿ_Uɬl߾1c0i$;w}\Af,5552lmm f͚;vʕ+ѣ bժUвe ers{njuuxQ5jV_];H)LN]5N[%KPB\\\PY޷;t]vhN#:esBBV&&&ԯ_s$''m[dk^<Ʋzj^zS\9Mv;)))tܙ͛(+7}H~$Iz WWW^|IbhڴB'bI$ +ZhJ-$I&bI$I 9K$IɁX$I4(W/k=yD񾷂$C%Kq\ Ģp M==)KRnȾ?d;Jy{?<ǃ$I$ $I$i%I$I@,I$I$bI$I ]Ӓ yA`A2޸cPCO%q-.I%5jRI̻'SkCORNM^EݽCwɠ~_툯>tAGqp^~F]Λ1n[jil,Fj=zP ) Ԣ@FYj,l,Lp.m9l-G'^xh_vW'L]m "oE`Y6_`o\Sѣ+F&鰲fӰQSMc%JpI"ZZr}w @(fHn? nTt֝hͫW6Ӻm|-CqRR=|Ƙ)[+;BmjxTmF܎㷟bWj-Y8ei1ݼ?gbBܪ0m?rnζtЊFeh۲ k"8=]nke[1GފS׮A-'V-_ @LtmZ4ģ=ΎV549Hӆ>ܹIjpv¥5}AJJ v'W9۲~L1W3f G7S8{<\nA}yey/Z0GYUK`VJy_Mӆ>xVfޜ_ԯM-'.^8[;omFYCT3uVMZ۠Q>0 HNNΪo:/M km^P*"Lcڶ5' :l˼9*+=ԬHJenyjÇP6~\ɩi?LHhլ> ԩ37)ƯgGBB_荣m9<]+R՚ukViwQZ9|Y?NeWzzz܊GReZ^_Rم{weNUE._fLs]s&Obw..<</X%3m[6DKK188VEG(@pժN]{Pݥ׮^ej2ߺ{x|2bŊ#\СSwϲӻggVLuW7_Y+V~Oxw. gCNJrYzLЉDFF9 y6ڱɓЫwl9;^Lp:~ H;~޹-ߎD@Ӗ^@f >J.=tu`gelӞbŊU%p:::֞W/ٮyxŲ$&%а<9|wOo,*XйkOF @BB;osP譁8J6̬*p^ TlʵiR[e]☘h.?˘CeEhs'Nժ3i(TwqůnwVpaVX{x<Ȉpbbʤ8{5we˕yY^ emI읺`Qv|y'''S’PןZu|TEi?=}},*%֯]ާ 25YpikG4@(6X*0 5B-gq,aϚ]j`mcݻ1PP e9}1ETsv2ɓp RRSx%w(Ffe=K W(*ū#oEP+m}܍[W^HĿnC3v]+Vfǎ[cReʾxq=rl*q15Vm?g,^8TFQw]"ϗ7_3'>2]Z !x@1]w-B7~*m&RD[+BNg;i.ʘb"6yg'ı`-Zɮ}4m7뷹La**veBߑjbtҝZ6[ߣ=sʣ'jԬw"R2ߝ{)wMYT*Zȭp=d}9juھoΔ2jDvaԘIoIסmSJ,ɦ-ٹ7r,lJHHXwr"55.nQP{S||3414+h:GwpAp^ÇdI^6n1Q<}]\9qq߽w<&N!pnJ{/V+72f\lB`ieEcAhiFGG9Rފ0–7ShεwN&%K2?}&DEĔSpaZnkWvA YCGfJb֌)|=Ks[0:9](Q2Yr7 jצY 9RƾhiiH*N,]~AvmDG[]==tu2M;i z쌑1&9ڦ Q]\m2eY|=ƏG!o@V֭YA%[{n@֟1l,?i}zuΪ6jRXb=Ť#)Zs!vwܥ{/fa]/m]؉U~wĒ$IRrYZ ¹%b)ݰ$I/{F"򃃒$IAr $I$ $I$i%I$Ir*)V񾷂$C%KBj M&O<)KRnȾ?d;Jy>$%I$I@,I$I$bI$I 9K$IɁX$I4H~״$}BŸr71GPƫ7nfXȿ4$I"bID36x4FM*\z|ydjp+%铔/S۷G__cccߍ74]wERC G'''YYbGk蘯*U;wPZ56l)Q λL-; dT"k:ؘҥKSlYLMMر#/^hgw͞=]]]%6OOO-[&ք 3fШ|{F|tީD$%%allPrF͛7ܹs,SNѺukM7Ev1N8T2{C{Bh:ܹs۷o͛?Z:Zlɝ;w>fb֬Y?^M&eYp>== (T*Kԩ+:tVZTZ5fذa̜93ӦMҮ[.ԯ_ϟ@^(Uvvv)yDDDЬY3\]]Z*/δPzuz聑?777T€HMM}݋+VVV8::o>>>TR!C(ۄ5jٳg: 9sRJ8;;syMwp!:+˦{@Rm(DtN\\888inWɓ'm\]] Ix~xˎ7ӧOWf|}}??? ŞٱL߾}TVVVHdd$666XZZRbEzEJJ[baa5*U_Uֽﹶ ȷxĈ+WZjqMݻ7fҤI|>|3cƌʳDlݺJ,ɪU4iϟ?'22/fر#'Nɓ=zL󏊊]vܻw!C0n89~8Ν#**7MNXd aaal۶ }} imѣG ̙3ԫWI&)]\\8pOf\vMyܻw{RXbeȐ! 6͛4l0WLdvLey<Չ'2dQQQ<~X)>/d%cիW\r}NF(\0*%K{nnќ8q^|ILLr|ӧnU*-[D[[IRR)S9I+WV+S 꾕&55mmHMM?Zj/>^]<==)xebaaԩSBk.gϞ}+ٳg+w>t upiܹD͙2e 7Vbgi`i}ci9[qRsŋ舁Ai^?_zN8A˖-5j%K888(w졡nUYN"66.]0{lbbbBBBaaa5kQQQ~=YrG|r*T`˖-̙3iРfffvѭ[7Zl'ʅ̻L6.]a 2\oڴZ2t͔)S5jUVҞQoXXX`>3^zE"EQw=ײeKiРIIIhii1tlV,[={yT*'O/@__z/SVʝy LpLK~kSìs---qrrbӦMY3f ={ds'e˖%11kkkHΝhӦ ֭2س:Ϝ9C^x:::t NΝٸq#Er9s&:uSSSMޏN%rx?}/9vI lG)rӇk$IAr $I$ $I$i%I$I@,I$I$bI$IҠ\}ɓ'%)7d_(Ydj KhnI]rC!Qʛ9Դ$I$i%I$I@,I$I$bI$I 9K$I˟A$`xA`A2޸cPCO%q-.I%5jRI̻'SkCORMMY *U0emk ^nU2]x C|H/]iiJG)p:24QA#x0  h[[K3щ/^|ս֬x>@F>e utq->'t>9|Va|TwD||O+}PSIZ.#7 n,0<ū9%WBfM$`yT -ZTʱv^' 4獫 ;oCۦJ۷Ҷe#9Hӆ>ʺ?~*vVْȐ㎏Wuڷihȗ^;м5k82n7v^.˫6-vr7ѪY}]BFxTǯ+GxkϟS~#dC׎ _uܸzo&yܪЮuP*",>J}?ONH~2MҲi=kנS77# r:/M kuYwܜmҡyW˕Ѷe#פE?qqq9nςp!:+˦{@RmШ>0 HNNH-sr nζ̛󫒶Y#_#^1*K-'[d8)c.!!Abiv fxzrf&iy{|Ɋ6W._={#m@BN_f{bs}ZZylu)LǪ=~ۜE kpɜ4{w0f8+I~y꩔iP$k6le<~[ub? y#x;uXj#c>֬ZƢeع7SRnZooF9'1i>:$tO_*ii۲!hEqtt.f=>sٱ'iS&왲Sl޾GN_qܧO>w /b¤,7cnx9. ;8*Ljĩi'̩KލAxqUO;F .%QQ tnTuJ(_f:vl?SH+g3FlgO¿^#_°_F.\++k=pn<FFƤ(QJvTqr@w7T*ĝ t]jj*>>3J xWҢCYlЩ;ZZZYHx* ڳk;S˒wpdӆ52HJu7+W퐞WVSy+O"/v={FMi0M[vGu{w(Z BO",+ct9wTֺطg''3n7 9VB _*ڣ^)Q$F&ѮCWWҢl4mILL4mZ4$99B7pH.oQʎUҢl~Xb=Ť#)Zsd&&ϠmaiqN-kѣ8Wϳi\^upv΁Smt4ƿwxz&œǏB߀> y<,(ܘ翐ǤS˼65jA85EKKD*WqbʴiIgлgg161Q^*1yS(^3L-+eh17nT~ѕoxq\z|7[j{:``Pi3~%)9 >:#՜] KJp!,,Q׮]cۦe"t({i_1J߮]8B/^_h<UnH7kI$iؐA}8|]==?5;bI}=v*7w򃃒$IAr $I$ $I$i%I$Irִ*)V񾷂$C%h987R Q λL-; dT"k:!99ӧcee*To߾ܿ_ӡITb;;;>| {{{MM4=p(Q$9ʎ; J6]Fbov tB~سgAAAܻw'ObaaAxO5k,6lHŊ5]`„ Sn]\]]i֬TZ(yDDDЬY3\]]Z*/`РA?GGG7n8@ѣFFFѸqcd֬YJ>&M333֬Y,/\0 2\\\ptt޽{ӯ_?ܨR  55{ꊕ۷/lٲ'''hذ!nz eX7p@&O U...xxxp1eoߞzꡫKpp0W\;;;\\\8pPp8LeӽgV6hGk:ׯj*VZER066fȑ TX^z۷חʕ+3l0/XB)'oԯ_ϟky?]"n޼4<fffΝ;9.pѣGC !?~hݺxB5k!C$ʗ//_.S>}Z!ijgτƖUbǎ"))I$&&jժŋ !۷oB+WW^ҥK+y"))I!DBDBBnС~B"Zl)֭['ܹ#ą BDDDǏUHQT)&bܹN:sbܸqB!^|)DLL...ѣGB!Μ9#}..\{ʕŦMB?^ǏoU_貣x\uc2A$$?~uĀ4FVvZv޽{B!EڵŖ-[BԩSG̜9SvB!^*ͅ"ߎNjVZ)>L̝;W13]wѵkW5}}c'N`Ȑ!DEEc\]]uhkkPBVZFVSlY(VgϞ믿V0ӠA{ѰaC%mǎ&&& *mٛ7oؘ 6S8p^^^8::`aaGɰ2۞={ҿ(Zh{qƌ?@jժERXjѴjJIKμaÆJQQQܻw-[PJ8vXY& .c5ܓ~ņ4^e5f r]…Yd w&..pe }}}VJ}1,:::rmM7'+ⰰ0V^͕+W4]۾};cƌaƍ8::f3MR2鿧ѣG2%}d ʕ+3-+'gUvʕ)[,vb…1B-zg'1}|8˗/vڴhт\_DnOIsT" J񾗂$CWC%I$I@,I$I$bI$I 9K$IɁX$I4HĒ$IAf'Oh:Vcܐ}=vdɒ9NX.麽'8vI lG)obx0SӒ$IAr $I$ $I$i%I$I@,I$I%I*؞Dr71GPƫ7nf$%bID\ alhRIETR".Ԛ?`%铔/Sa]/צ;;٢z}tfsil,Fj=zֺk׮d܎dnOӔFRteji F,5Ba6&8XѶf|٣/^ȷ5Ç)Dɝ;,_.zi>IP2#B24ju!*Z+zʹn!_~ꏚ8))$n_8Z k^9fiƠ+_|ޑ 瀴*hKUknEӺy*W*K=_,uf|4k䋷{UƌݼNQ-9ҥC+Ư++-mF4I2O&u9t`;ˎ]\lpvbY,Brr2f G*W*W_vS}9Kgq wؚN |;|0Vvފ=99aٖU*"l>V5ǿv jy8j vP/hOnZ[0w6u}N6Mu8LeӽgV6hGk:8s`{u@f)gljcY NMgtъ̘SIQb}}}-ZIVY3zvmǂū p_e0Ӿ۱߱m!z_f`dl?pb\%u=;Ѵy+.]æwQ jټ}?s5~s8{56Bfw׿kV-cѲuY)7Qݲ*{6hٖs8:$tO_*ii۲!hEqttw`򴟸nY39xҥ?5]-ICej~`Ś@3aDjz)0q5g[XUE,,,ٴ~ f%ؿ㈌'&&Ǐq% 8WaaaITmm*q ƌJLLOSU٦tisJQŠę~UκeR.wDҡsw0vGRi/42q{.mNee9Ǝ׮0lW_ FRe11\<1#(y)͝[*U@S)[mVпnCcF 3:tt$ @|^ *> (R,WWbi`G)O&ؽ,pT*vd>={w׮l'g}Xo\57o\#VoMut=&$$$@2?=}8/^<w~PArzGK2a*^z#3qqu`Yե6#op[А1$%%eHw)ҮC~7 w7&˶/c^r,T mJ|]<|@3sVLY0wѣTA LNSO܊Rs4f#`;8R硗.0mx-[,Ӓ;bJ07wnNQRE*8#IDAT?: V[0–7S(OPP!_Ɛǻ6f-_牎zz},YC3c$|޳^u67`zaddi'MA 115:GeV˻ΎV9'$3i :#՜]l{+72f\lB`ieE4!*6&,^… д%11Ѵiѐ$ Ң!9CAf< &8&Z浩aVC¹Ԭ/ZZZ$%&RKWn<]61y)m7Po~@U4@%D&9(ajxKXRt^;t⢦3W(.gS*d;Jy{?+ݐoW.ᷟgS(f}M$I$r } ^)(>aI &AI$I 9K$IɁX$I4HĒ$IAzYKx[A]rC!Qʋ%K8mdoɓ$CWaaa9CrjZ$I4HĒ$IAr $I$ $I$i%I$IwMK'$i8!w9s5jxf接KC)r ODHL0cGJ*jԤʥwORQ>I25eUFJʕ+Gu?JY*|oٲeܸq#J.O}pttd֭<{.]`gg[iښJ*믿*yо}{|}}\2Æ cСbkkˊ+4?)PYY6{joFp$׻~:VbժU*U cccFwQ}ǿ%܇x x+ޚWijieeGfoefZyhނG * r_;䖉+j>9;g3bp~^^'N???bccصk=z`Νx{{3rHuFtnt^(EOgϞXAWEQ222f͚)w7mw*VVVW-(J||(dff*JLL(w^E(ʶm۔1c((ҥKӧXrJ]zJl@SEQRSSGGG%??_)))Q233EQJ{֭[klCXX[>}2c EQ%77WS~^xQ)..VEQ[[[%??_QEѣxbEQ@177W~EQ%!!AqwwT].R?2/zRQU|t|{SQOwn+{UZ2geرu]tKIKKSEQ>#]Wqc&զM}F(1DPP>>>h4ؽ{7AAAt ]͛7sq@yy9͚5&nZZΝcҤIX[[3` jM6qȪU),,$994EDDelmm5j۷oW^55k24 666РAC֙㖳>m=A^3te4jfEz///t@׮];5/֭ӭy%ydq7ngϞeΝ5&J]Z]=( XYYXNUmwXXX0k,XnNNN<׽xԆ{!C0d"##UV7ʺ)^k(_} _ЃPD8XfMT۶m'++ ''711i˕rW⟡NfM&'Nٳ1555ݔ椦pa [7x`-[&<<8;;ٵkm*((`۶mTTTкukիNNNtIIIo2NJ+w]ׯo߾5bСXXXF^^qHqS =xB|[Z["ooח2fݨNnn..111uRW>uD/n:5kǏ7~m̙3ܹ38;;֍1tOEEc̙5ٸq#O>$sEQMF= j9OIN"3Sð6bIIgHOpGvN |f IHm"6Е+**U7'YLydu[H{eII 3?F&&)Yeb6뿼"~ؾ%: JKKy)p#/c0mB:ؚ>:!}-{vw彺x5sKߢ{>=܎V>ݳN|0~N.EY뺲yL:Vn=wVyeMcfΞ9ͦ XWȳϽD@P0>=٥#cŋ@ҔG1t`/՗giv(NKl[ ؤڏ&.9Aiל oCWmN<:e֯?_ GUK5K,_sFtչR\\NS6cq̢mtCس;JW7ӻ.s)<0º-=֬[}ztd Wܯ;^p i>_n 899R+SqttS([#vz&fqrc⣏5b' Z;c7M}RwpU*ǧ'8^-}҅9|4Y`U>uE)..Tׯn9`kgODDcO:E:}GN쓕L4?&&=#GslӟzEoO+"wư14ilݼVpo-wf#Sxoɢd3qN0-oF;stg _{?VA~yߏһ5#1}G٫׭oߑ-QDoD51ΝKE)c`'> ǧpX"Aj߬eڹD`XZZo֕cW}`l._́Ka#Z寷uORMr]A:҇ UUUzm!Y>z<&Kwi[324ϪQT 1o^KHG&?Q;"Wtײ!CQVEޜ?Ӊ C~v5]_t%bmzV4tN~W_zNW̜ Rpvvayr~GWu {c;U_,a]յ9Y(JBQpumt;eju^F+VrYYWQ`3tEA1+++J}үT7(jyifj^eMLLOquWx4 59ѯ7v 6v3z~ےp*,YjxٛR1 #gjF|oiֱqW(BvT\LiY) ۙ{#ç%.ҳs&N.D9ɩq҇ܥ;V9[£ʫ+JJ1@Rt +u,ʳv]nfee9:v- 7V( )ɜN<[F%%%$&ԭbXXXIO#??)W~]yy|[zsO?V_bI8Oǀҷ`^0RNsȭ&yB|[Z["l͍̻/a}2eXN^n.}.߀A|C.TO:֝s7bggϹs)@͉*;; o?r©7 ҫnX\2⒉;FM}٫/!ZEQafjF~jNum'+ãI8O#4iҌK'dggPRZBee%/[JC7V+++c͗+;`_W˕Y3ΟOen竪 WgWم/Tyoèprvf3Ξ'&Åv__n̸L7a_ߊW=uxqř܅L2-7acPw ^Y$c+T*5}=î^Rjv:fe|WX߫9== ڲe݉a+8MDn r $lO>_7^c0 P;QcFPYYI=Mc>L:>xyo^;?b+kko3>.lz{Ob57~{j  k?1zu A7[[=zE9}|&^}9۷DQ<=>q鋽ޱ$y8,C[c@S5?>̟MPgN"G[KmM`+Wc73rb/-y~\#C-G䡔ZŨR8E ̉:rŶC $EZEkO'& jKfn1Wܴ>ۍY==֍9+pEʰn-H!%b'8֧ɭV%asnboP+kcsۺÿAq.AHZ'qA}3F_'O1lbB1gx<˾/Pߢ#{zmM8A,Y3x6屡꣺NL\:tmטIaVW ]͙7iY|n(!-uv&гcS*+^Mx~\0? S61 *<@Nv4-~ūhkȇuq][s<ٴt A[ɯ4l-OE6Ҍ\-JWT lN;/gZs|+'h=hzGRDor|.{_w@3Fք`xwo &/!+F4s4)& ls+_Wnb ls#psfPh }΁RFڜ $ĥR\ȥS96ZW6)-8{!Ww8=?Э-Mm`Bԡ>_bZ2}dGTdmilCgJ+8?.(/g0{n>xޱlih @oԐɧ\*%ylWMI.ryΦlW'sv%Stk߄-x(~9۠W(Փj&jUZVDZuꙨi[)"@73 )&+/w;ݍMĎ=~-+eӴhlGGoD|$dlswۖw=wլ'nbGkZ.\fĬR6E%׵?ajnLǦQQY] FaXzERE|b!\mSҌ.݊OzjǦMcw'fnޓH^Q9MņVKL[ҔmWNw{NDLM4uᾮ-u kL^aQGRaO"6<6=>M p#nVpJ$":Jl, hF¹K|=<1yEe>a~cTV **EQz?B! ə򗵄B#2hhZχg!BIB#'b!ˆ$ !F$X!0"IB!I"B!HBaDB#D,B$b!ˆ JZ)`'1$ĐCbcD|CbH !1$B!OB! Dk !(*EƛBX!0"IB!I"B!HBaDB#D,B$b!ˆ$ !F$X!0C/g/U%tEXtdate:create2018-04-12T17:59:40+02:00i)<%tEXtdate:modify2018-04-12T17:59:40+02:00tfIENDB`django-tables2-2.1.1/docs/img/bootstrap4.png0000644000175000017500000021465413542204315020177 0ustar josephjosephPNG  IHDRlN5&gAMA a cHRMz&u0`:pQ<bKGD oFFs0PctIME ;.6 vpAg4lqIDATxuXT/`vb ݹvwNn]][A%q`P@y?Ľwνsg\B!%A|@!BI B!@@!B$B!H!B! B!B B!0@!Ba$B!wB!:ɒ%r# B!@@!B$B!H!B! B!Bhނp4 Zos굽^}̭?l}B?WLN:srN|1 ́}< V]zOV^-"ڵ֏qRlm}\TEj ;b3uԭ0܏V,~>9G?]j&6Q2X! ,` pS%;䶎C=A` pY|rGjqXR7V! |׮]ԪU+EBG'bj=릿ז KXwQ^RyFɐ|__BYvE)23ςDPO0 i'AᳶM6BAb6ꬃ>.+ͷ0yT_zœw=7RWǢryfCQ0v5[cT*̴p3bǯvX{1t#|q 7~.$gp \{a?}(P=t}p˽Q|XUi4y@Pd\|ܷ11SWTUs4 XM.v8z:Ae駨υ=2Q}f.hT,yq5רB!NT=q ~|ԏȣ ۨ?@%p ̩ O('29-fO rYuT)ӫºj[VJ֬jZnNu8NK/п|ao+[Su?T%m*-6} `m}ov3{i̡EL)T%4MUjCժF%:깪Tpd&2crHU5RÑP}0,L< M `ik/ 4%jAU8=@|j٠m[ ͡L8*;C{·5Wձ:VW]*9Zn`Uk7_XR9‹amZr١U3`i>YSB5*qZөwo*w_XXVnQ}VG;r7"&8ǜB!"UV1Ԡf麟P?F.=w6m|v:],ϬeU^m4;t}:hAZ Δ\Utl}IU*T-!X rVk`ȏ1qWANwpH$XUÎ_ lEӫRCp58eIr U_"byFS jVjU/MHX|zk[Sв ̭WU.猐gN^7q[3@86ބ~pHBK*P}w!8XQĂѝxB\IчP0~TY&*.n䪒9iGK9x|Q_ |Tɰ 6*7D^)빪u^!Co4j=TEXpShogLҩyZCV\JdVZ Zêeb>z j[ä޾@@W˂'SUO_TeEתY3 r+c-rΫ?ꜟuN"YZ|YݲdHhϕW"oƂo*NOdK<>+~EC_72RǢ.m9~yt]~i%MLt^0|撘s% zW&?B!WPLP:>_ΎlE\/AF"6 أ@F׶XE=7/\+7,6IU#__/i"0MytPJh]Hk[C"9*TR@U<*.W+TXu-"/Hj_xQzTAS!;L*iᅽNگ MJX34ksm*cUAtRH$Pi*i{MbC{η,ZdK}HsFgD3-Uz솝AEҫyх1{Ճ4a* ;b?6 :/۰Emc_0Z3 *s !>1˖JP< W=Jk/22QH*+)T~*M"\dTmX_t*n*!u$<|ݵש'˩>oNm:HtA?32R-M T2TdKrCTkPv)<15(B՘Wz+3Wɪ>.sMsd׊9"^*..՘y5T8ӏZެ]wÞPih)@? L#ߎGԱ^Yw]3=B!~nZuɕ .EU+ 4V;!l8JWC\}]c_].$~u63Hd).[fU-c+kXڀn#q~R+ Dl,j XFu;JX:*s&~YH)4ya&_jU|S7T~̐?nVna8=vk-'1U-- jgQ׊s>Ӟٟ"Յtױ\W=C#̆W&}o^7fP稛Nlyq;3x8(X.4Pz/c,"WF/b#p5Xv'?~2%9*+alHd Nԁ% nR0f[+UUP-kb,jIU "ߏ0/5`v;om}w5cJ;0g66S&Q]a-9RE:]{%q-jT5uשb%2QW҄!S^ N΁ Bj:ʾ!0H̓!YāR9R<״2 R$Q_/v(mJIJH'ashNrlڧE^zVcA:A#˪VGBP83$[l]H9}e2S< AAWM۶P:h_Z}uSLP&PJc2.bv5ФjΏzii2'@Z5H!į-=N)q4':hsM-lTVU}vbt5CG4q0)\\AXIOW\jfvCe>(t#ۯ~W7o uV5H0h*!)5S:30zvUZF1k* \&U k͜BMQZ[mQCmNfqDMMz tzQyﻫ0\uW +S+^jUk#5Ԁ Ԁ* :vj!|*}A\Z{ܣ?E3[`?"U>,4̧ 3 {> zA1ef ڨˠvg>:<`A55pdI^UigJ5h|R%JB!~mF!!!(0XM6.K$",ScGQToW9]Tx8$<#s9B?G!yz$t BQVZPB!BaN8vjZ9@LԫwɄB'AB!B25H."B!@@!B$B!H!B! B!B B!0@!Ba$B!@n&d#'Op {֎Aͺ9r1H+WӼ,!brZ$L~拍;-v-k\4i S6nSx ~_`ii}~֯O2⻈ѧ ?A|ys<%KH< ". XA4P37* gzg/~lp]|J!Dd~Z V]e#?kPRErE-_fz¼~opaWi155r^rpK޻Mh7io׆V^]ᅧ&-M|,'?^^(m'~,Q|w6嵣Br[ Bi@T)I4)^^^dɒ9?m?33g/oؼu'Nܷ07.\~oۏM c`5 ,?+o߽h7n  Ng/<n_H8^''4&a/s\{C=7;vS4jP_H~;~Z `anΖ8r8YdJ@2vD.̙5T)Sm;t۷Xz-={t<^ʛ> \ry嵣s|(uveCÂw> qҊ9R!:F g̑#Y @„ 6e&bԈ4nn޼s1x+}.'U]UgG ?K$[bܹ{xxxښreKӣ[ׯ\7wwFF#,l޲wcddD(]۴&uj딯\MOd|EK8y4o߾֥̌E8DZ`6m۸AAdɒZ5ӶuKWƫׯؿg'lsFgX~ϜW! %KmVؤK]oM[2|dLI {W׍>zGEdȟ?6r |݀V^˶;y MMɕ˖&RN-b?7oUkr_܌ܹrQFuԯI䧧֬c<~??RJIiԠ^4{kn noooTV ###]@ҤIװ)׮_ԔEeZ6wC7nH7m‡)T\_C"?D(GRj]vH-{ s *D/k@nkm g¿*( I u`ezCP$=,tq;-?c#]7s>g ɠT u>-6úq;pJdN`֮nƛp%7 4̧SpoH:V{clGo! HD5K@hfUXFX~%fx)*V-[ٴe+,eص'OҦIڶ![zPOϿY7ҒRJЧgҤIM@@kֹk>zD``2fFtЎDjmoRҤIYlW^ۇ֔)]:zҌ_uNիaǢEhۺioS׬< Ȕ)˕]V$O{FjWec`-s)S߶ٕ޻={s->~DDș3ժVE&$LE'9z6q}>|@)(\ ͚4Dqi1t1߻o?}"(H;mWXΕcb_@Ŗm#{Ɩۙ2iBĘ [g¤)z$IXB111puܽ!GjIRL)Jw!qb5BϏ~~ 0d 7M2h <;Z %KFhӪ%y_ܻcɲ_}݋_"a„8.c"z{ϥˉYs2\z֮\ƀACCgm`*Z6{z၃ZMe(_L6Nښܹl/K@O8w9zLWiϐ!=3ekժY!ЦUK{tAh֤Q8x?9S&\L0j #4#}حdIusFHT\ J+Byt: ?Zky0K{ܴzljc_ރuqm`jx vzQwv~ Ž@H~`~š)z-ce| tN, .{aN&ē B ioBP¿}}T[ɯf - qyp.Uyv٪mu. E z9C֔j-=BLGh؁*ʚ2粂A4/!}Wj`8~ Sɒ%c7uE>|mky\4\2fdd͚P˝K bƬfH_7GXXS9r-hzɖ-+W,/_nƚQN'f񂹔,BBBy̝PׯY纑F8~8c%+oת/U+T>F~~~1d~2=sffдyk>|R|֮G.,l7a„56;Wᯋ8qbm׎Po7sz֬\F1'm4zzF6:}_dFMpajjs5O`` 'O̝fM7pDGۛǴuQ:kɉ.??}ؿ/-7{.O^wV5A];-m=QD̙9]$m4;:ۻfJTss3̜'nmmk޽{b|bʾ@~7z&;&hW_̚ @L'_N:rmشYӣTġ0}zTW7Ԟ4ܴ)NҦ0^]Vԫ[zukSJ%`޽{m[G8\qS hZ>~+W5ގ<͚j гxK6H`kmxuw*jP-t >!*=LA@@;O.l @ӏ. 7bdJVaR%7~A]{kL4tbލ7cV{3tbr?;tzƏ @4͉?{<>~t[ Bj*UƍדFBQԟMk*wRvݐY-,ͩ^UoeT8ٷ07gz̑^=i~GvK,Z9v$ƌ-@uI2X6X/hԠ^Jdzmgz@LLLMۛ.~?u5GXXg2e v/_BBVVVK<9fffn5//W^.Y^9L=vBslҤL"2ls\s/p([~SByǂ!0((BO"˛̑=;9gÇܼu+ZS>MVr-GMmk]ܚ>dҔimJӅ5kO4~? b*beiIܹs.~~~_ ߗ4&ܱGhv]022¥X;&RD"Er$I fhY̪?hwmzK5OQ rA'?D>V!*rz5WhZyߏP>~P)!!$&cAI"F^jsc C]aH,* Y?App0.??ls,Y2Mɭ[Pd%J=,\HRӥUo& 1J}.Ѻadyz^ј #:..Αؾ!_|f/C8y zA1cG.^*,v_B'|~n_:O=7L78 cjjJA{{9 ͛-a8~OhjJ̙jyzzv={" & >?:uZYΘzv.ۨϞ-&x76o+|*bsLdɬ ޾{y;w^bT\Uʒe+Xl9gɱ(%PLh[H/f¸.T0ǡdqܹ tB51۶fy=׷Q2a3p2Q)ѕ5K퀀>~x b=ڳW3 Nbq>x=!W ǏߜGl7m4]of S֮ss%کAh+ff?|)W*gQ?_4csu֏:_[_  FVmٹ{W2?~C=sx!\7еG/Ka5|]9AoYF&O@Ca'ONeS._F*G}T93@x)m-ܩ+HKHӠL ~tJg;^pPr QyqiE;fSAQTA@lY4c4ԌIWa?ļҢ+HJ?*@P7ww5l€?pX ?\t| =ۭ_^:)OGeTVVs(_UlΓTχy#.EP$%Cn=59sO2jU%]?NQT9_caaڏNE+S(WdddDS6607[ԩ^헴Yo$JRߴI#ru@655k玴oۚ'Oq_O?7o1xPH{>FΔӵsHSӢhW4}TP|9̀#Gk҂ tUɩ |TևJ {ԡAqdi5jVDŽ,rwQ?ֶ~WP#w̷-_@AԌAc{XbJoδ0O}FcDzTWړ ]ڴtqp2QM鷄,- :\36E݋reJcmmE„ Yt9Nutgq.D߾֌շ~vg͞I 3}l_e_Qc5_Jh߶ ٳeӤ&wa yvIr16°D;Ȑ^̫W]SǎԔWT66\ð[LdʔQֿ{n`\ٰqvm4wl-rOh7:]ɒ%L37]ڴ^T.:&LHc MsيtVo3{nѬ)8qbq |%˵3gq(Lʔ)4+'p៘\%7P-a*& ^{?cWnzOP<p3L+\ mZ< o;n>H.i i= u[$}G4iDhJ*A L*7y&`)-SńMTG##XGv߸ 5 #;~256n٪/ssG16ts?۷sݿ줹}БH?߼} i-Seɏ@m/_<߽ᱰ)'b~9i[GΥFM[Pr5W7@ƌ8*o#_m T- ,b״VW㏐{m1~.a wHlF _)I?콫 Ԡki̵Gexd緋vp6nr[asE>{^ 0vyrG{z#nov8L\pK{v nPPGּʖE&GwMn;mN*'vyrkr՚(v/"~hfffT[rL4%B+:t=wu .^….xQfuMa@@ 77o:|T7__3?KɃ>j|2]nȾ4| ܵGǏ=B p>~C5-)S*zVm + #Œ5'Oi4iZu%KOx /\r>̿v+߾}j~(*V(~Xn brEl҂w ?xI1 :}`Z|:S^yz hʣ]$tљpEO ok T4b9u~OW'S-wߴ׿3&|Q=>Rt2H<@J1Й%?:Pjɖ 64_>->x0BE%]tzҦL!uӓ ~ՙJg[aS+Ʈ^3giG6 _d7o;qK}_ҳO=ev4l"ځnwͺobb°зw/Mŋ4Knqi+{yyѧ#G'zA`^Ȳhи94  ݋C/ 4Ǿ@~&Mʝ;wc5brL<???iֲ NE9x{{`jt٤KG-5W;u %J޾~~~\v)S0b`P±')YE8ſ/'˕\lq;&G_RΝ@``֯ܜ5z:@8X|pw+W#~mڴWRty Ԕ׮-v%ZUmԠ>l/U"EH(!qhʑ;'N ѭ N%00+WQbU*T(G)xf6!Pyao޾uz7$9bRv7,IXO %(Q"3e$qĚK^Cjkkϝ}eN]KjkL 7+]Ь*1  w_8T;(6eb[Ce׬ZfgNf3sFDj)8Pjaehc܌pꉚ?f}ڇuϖ L8zվK mMk@} fA*mc6}? LuՀug j/T%mN͜W]Bj$O~}zi_3q5l/?}.eKYW4ШAh(#Ҥy+ c.-==ɓ>MW/ge˝T!2eÇ#T#>[)R$g쨑4ǏPZM Ғ_s]g )Tlݻ̜ЦU MN{3^]O`ɲԭ]Ks=իA@T ǢE K# t?ʏa ;R'r\~k['O΢sݐ0k4litm4vOOOPN-5z,E_v-&v&NJR͛5aF<|H@@GujثR"͚4֌+|jK`@3GXX333VXJ}4ٽy7-gmmiS$0`N SRvJհĄtiҤqCP֮MTR+KKZ*l߲ݴՅR0S&PA{H811fNDL'Ю YdɜD‚ܹsѭK'أwuUl7kyԔ$I'OnLLTl‚Mk4y&a„$JǏSΝ ss&LH [׬dƴ1b3g]Qw@>$Mnbbˆa57w?v̤#昚1cڴjɾ];4c͵~Ï>O@M[q͙I尶Ԕ)SPޞsx8xaG8:Դ̎1F׎?AK_8s2~WXӬIX}! ?$ t~;6?ݻv]$ DDW)W^:KGob}\{ӟߴaf5B%ukK2V1t?aS),B!!b[{(b 1^}`=_J78sImط #LV('}B_qnP:Pj !#K`>?!Pojo8jLuցp;B؊iK“%}u \2U|˞ R!~'w`au .S?/=XwI# !WtH_a_}8Ƶתh8k{1vmzndcf2:;FC)aO>j˱$E0d]s lN0$*C0*6ksSL@8x_-҇r~&Wrwѭv‘.Q.wy[ݞZ>0LWG3cgkixGX! W$0:v]ig`5Lǵr=.<.C#׈۩~ ,{TUaF$4'`a|*_.e&?<5sz;'PɑGリn@JyQ矫FY:"C2(MlcÃ걦cwԶ.@}p{ g| aBA< '&Y|D5XW*,O <o/FUH`UTQxUBKDg"6钪4ڀ>tb3=;z"w]5`!?ke ns 2Y/=auu{?j~wiGC^C]>B!wXsMUAU(}8F mkX5TU"a}& v3PySNKqyBħiahip&R&TT1i,, 7kxeЎQEҨƍ7amk4CrB!W LM{w=t*߇"n6U*Md0ya>HHM/:6wm{reX\[ZeA5SB8+` 57U'[<̢0dMR2%eԺa~wQoKL|j2lAB0 YB! Pi  UBoT_z5.]:ZO!qe @"!įB!q* c4!5H "N%B_B!BK!B! B!B B!0@!Ba$B!I B!@@!B$B!H!B! B!B B!0@!Ba$B!Dww⻬B!q"e.?!Ba# B!B!I B!@@!B$B!H!B! B!B B!0@!Ba$B!I B!@@!B$B!H!B! B!B B!0@!Ba$B!y 5V.7nފ2oߡuvtSq˗PS͠s|G6[;VYE:vΐa#'kLF@iz $=7RrVB0Y$0X=y$E{> !D| w.B+W011p.R\̉'N>KUwȈ3ҷOO?@"M5\)^׵֥+We%lv]G^=زq=`L6 jR:uhϼXΕY2Nʕ-M֬Yؿg'U*WQXE46ekdzWV;w7f=uL5;kVƛoƋ9zɓ\/_Фy+.^C:i<߼Q{癳c 5}*W^?>t0˗,`![kׯӶCglms|"Fߗ.3lĨOz:Ǎf?8r 2dݻvf5+Ơhն̚1q9Yw>|rٟ8+F}}N!V@0(8e61_/7,UrF|8,)NbmCOv?B_9T[ RV0&~w~ k榐Vxk@96zz9mdM 5֨E! MME PzUF;___U ҒKajj @LhնǎBr~ѬIc@<}+WѵsGgFԩȑ=f3E:P^.89;~V-6'I-(Q"rqԪɠ*\.\Ӌ 0j| c.@~@+V x &{lj)U.]c}s6ؤKz[߳ۜ9011|W>ѮMklllȚ%3Ŝٲm;'O!O*{6Og[Q̶)9jW]#*l鿾A`dYu|vH1N=!mR *Pu00h&BHlEӃehPjs(4p1T~@<V_ŵSQX\Pr 9cu'i@@U(q"V4+V`18qիr1Me 4jPO(%]@eJ~F޼}Kz+yp1ZiO/kCe*mlH&HM"]Ȉժp˻w~&R)SLҜypXx)*w.OHI///zȿ# gϝ'GdΔ??? ƭ['IwJ*ʘ!=VVV̘5Jw|2/[FNѢEXΕ~{KLI#~l)&J+R7H9#\&3B\Nt)Q3/*xJ=UE=W~yL׿ QXƟc1WY|;fc#h:aX>T>fd- B]3LB_>5HרqK<}epssrХ{/h\N^jUy1>ȱcLB30ÇIxZ[Y)qean nnQ.4iqL8;нkonݝ'!_S+ɓ'׼ffflٸۜ96r Niݮ#wjGEҢi֮^]I2=崄b_*;c:$P)<+_%[H=%*.Oab%81irJurvG8Pw Bgzvn fʼnFj3/]ݻT*tX!GFFFze{#oe\oZ覝)I372FȔ1#+TTuH7λI*􉘖Ǐb*}.,,̙5c*F/y0ve̘!z22fdƴs &NJ9}(&lٺ1S^: cȖ óИ1,&6TjLM2w}\SSmYFs& t)xzrfGejp]lKW(D}1ksfNפNj*T(G@@ &LCaҦIyɑChfE?|c۳0*gN:t)S mZFɸ`OMԠ;wm'AjV~FnQժk^Οx *֭xq8u4NQO51l`ޱ::{]h|C!p~fnES2OO5 O Si^۶R՚x A *H]Ƀ/_CPPFhχ'Og1V19BOI:oLV?/4) BZ tA.4^~=vCvSk~.T MzoBt"\zPFuRJ  )c?NR J*#G5M݃z ұKwZ4kwNI"+[&( 3~"N\z5kcbRelllp(\ϰk^qr6T)Q܅B)S$W&{lazW>|D94۵IǾٹ{'k̔(M[ȟ//Sf.5Ũu]ZU.^eTө7iԐ\iӾ#}{"aBS.^iB(/Zā 2lh5i̫ׯ`a<}Jhؠ|D6[;֬sa+Xe+e~oOhߩ )ZM󼷏MXWj5pБ(7miWeYtE^{(ʹ +Kwp@hݎ;wm۰ SB?]šu9rߗY*s}fRf,>@-`9,@p7#0J-z sΫsԲ)AIjoC]nT^X0@)d4υj^ FCi6އN6[>B@ iҤH >̿o3yt S@~ro߽;88PKmNmEgb*Ŝ8xX[TZPiRȈ3ҷOO?@ݾ;{:WwujukYx ]ѧWl\ϠXlS͠qZN3o"֬s%k߳reK5kIcTe+V&MjMDZ5Yޕkq]ƍEn]8zg͎v\];q\caJq@@vÓY31t Μ;G5۟Ç2xV.bѻ/oN1+1seʈQc۠ ml1u2U*Ud%ٻPjf-3Oa$H`D]TPҺ]>}1#ޭ33˗/zLiҸ!S'Oߟ^}] /_ȣ ۨ?@%p ̩ O('fp'13 >~u`zU8Wۭ^{O?8N> {x𥳐hv܆U`k(movӱ!اnw깿CՐ/5msk}# !}YjP+V1it2g022}` .X U"&nϘ6ǢEH81gȞ=iI~: U:]v?BC04̬҉L Qz O?ZPV=25W,NK8'S@];8&jUR/V^>veeM1N=!mR *cB!b/ڵjRp!>}Ɯyҭ'ׯ ^f%!!!lҔ)_+W3sh/*Wc8q5Wc*hMh:shԠ&(Q+KKΝt0e˔by66.Ge<~///>B޼v?xXZһƆi4A@ڴip!FM$涑Kd̐+++f̚%Plʗ-Yh"]Jx%~~~xy{Wtʊ wҤI͇07'o^;-X xܹl5Aڶeڛ  ĊP6&J+R7H>i XA\pQ͚Z/?U[H8.D3sW^ĩzGbL|I<9%KpaBBB8z85W ((///R벲ӧO?,nnQ.stcQ-])U?p:9װ433cuɰcpp*NvGEҢi֮^]5-?fwѪM{._€~}ٺѕE-C09-XFW?/lnz_WϿH&Kk_ZeviNUVM+儓OGv= j}wc@{I'mz$:N{nj|,2.!%'Oqrׯpswg}|qW,+Q'OKy=U ccc,IwI2e322UeXYF]\~ժVG.kG I$ұ,S\y^k}1m2W.cZ|||hֲ5>>evIzu͙ cǟC._{̜6E3"mhK~ԩ:XZoɖ ó-K3 ̾ m^Pvv @o2YrNpUyhŠ+0<.|TXs %RJdVen0!"z,8xF&00P,BBoO,+W͑#*T(G@@ &LCa#pᯋs1Hgmeś7_ һRLAڴBiu+z=,ܹ{Wo; $bomm7t500NhfmI5!A.T]/^LJ O ۦ1~2xRXdQZÿ .צʭd y稴FIVi=y!cr~D'6QRYT~^jv_6tzE|u!YeMٺm=iFxyy1s\ ϧZYYQf F/Y~ԡ]$t>rQÇ=׷w5lJ.iѬ)ܙ:m&E8PlHW 7ld8;9r5֬]I^-v`ccCB<}]{2_ tj)Q܅B)S$W&{lazW>|DN&d=ȟY2S 7m!N%~10q X[Yfz^~5yqtћm[I& ٲfԔ2!???/YF@@?vJڢo`ZhƃYb>ՠ^_HYM:rӵ`H~Q]P,Sruq0EU^~0$2ITdJleYnh!hW˯@,š>TDI/0۟hj RSV {1#cY]<[?ѣW_OBQ/]L7k2{<:vΛ7oXdٳg2ժbddI =׬ۛn=z1m,U%"lJѭ wܹs[FXVܾs]qڷmMڋq8;Qj.^ӫg58ק j6oۜ92l$W^o/WY2t(,-SԋY5m҈Եwg q( @6[MI*W. Ҟ7gvyr3aTXM]V ?ǰX`~~~ :SϰheS"fff4'C򿺁J5z1B5_)[ĈsIԮ(qGTZ%_驙lӞ[7Q .B!~)W^:Krҹ*D88P3&M -̐#(X}TvTRy)Y,"$$֎p]i;#3f+Sviݮ^LA)P(9zL>'ѫ/?~tߧLAhֲ^Ʃghиyѹ$§O_~(Lr;!?|H`WoJ歰+;w^x?K^;8QPQZnǝyľ#E`CpAo5\)_*yZ:;p0F (S2v SFmv٧kl3^ތ5GR(Tr?H.ԡgz6}}('VYGa. B!B.NOoi3y-߼adʘbN^Wg*Ŝ8x1N>KUwȈ3ҷOO?@~5\)^׵֥+We%lv]G^=زq=`L6 jR:uhϼXΕY2Nʕ-M֬Yؿg'U*WQXE46ekdzWV;w7f=uL5;۷m)ƳN,,,v:m;t6'˗,ba}2FҬ{uZH4i;g&mZdџK9Y\vF3|9z? +m;kgSIocҸYK>}S?w6 ѾcMڛL?Çr:w9/,[Z2bN}h61i47msɟ/ sc#ר 9CG0t@f͜Ɨ/_hնn1. o\|#S(lK02/=j!_j քOGڿY*=B,5(qĬX R,I-,رm3 &25We䨱x`anιZ\,-YddΔVm;p */nX͚46g>}ڹ#ٳg#MȞdɒLѢWNE9},ǎUfIR$J\9c\:j2h :*W b񂹚G?W[YY9SF2fȠ)5+.Dq$P1웷>s6$H[HΜ94@@@ .fЀ~lݶNls WOjUjTgİmn„´))?25j;u&>|m S m؄o!mN-XDЯK'[2KE8},cv,Qϟy)%D59K S&yrba} `5$N8ZTX#p)jT(Tt8{%`eiɹ}W $1*[7l۷tvy8r-۴'xyy!266MܥM&B ̷Oo @D%ڴj^MP,=wٳ9SFM˼CBܸuKSOZ?:녱07'o^;-X x&=C9] =I;wEe=}$|^B|x:f̢Z*ͼ93ct\}u(T۶cceː> kW-6 ĊP6&J+R7HH\ۙSD\}|/>$p6[-tot !Nqlٶr1M^)Q~'Mv?|ez6IDAT'FFF4iܐ&;waҔitD|f ߗS=zu-E r+Xs % )Df[~'Mm{&g2wzȘ!CP0"I&9''G>_qb7Y[Y[*:t)S6z}`ʘtܹ{Wo; $вkmm7o{ӎG7q(\0@;vQ\E(ZO>%C*h˘!WzN7—7nSKkeY1j,{QI4Γ:ϥMhצyp}\c[Ç(_/qYdfԈRյ*'WQ|O<}t IBG^,s)PCBXdI&N^"K𑣌>Tﹾ{PaS:vNfMqi3)ZāreD]Xa#cOɑWfzL۲m6668.gصg/8t9t*=h(B)]+W=[6ذv+>"g4!66w ;w@d͒]ظi %uܥ.sF.U5\PJ*'4`(Q"ztJmo իL>:kbjjJ&X纁ң{8t([nCQto`ZhƃYbjYf;v[2%cllL3w>q(_fh' y H81smԯ[:gfBD:|2wX#ε`V<SNA oT)C=wa QhY kgj׵6B񻋳@ڊMkVAJ83cd-^ʋ/I2%E|"ϜM.ݱL6Zнkg@lr]1r ȟ˖|zV,^00drcʴ4)Sq&LL`bbB @M ף[$IXOefMLdAX帻cccà}iѬikl}I +ɤ)9f~~~ȝ;O9+&N6IdTIpxs0 lr0 &Lj+m64BB3[|Gԩ]wQB!DKzj)]t1B!B_B!B8# B!?JzB!0@!Ba$B!I B!@@!B$B!H!B! B!B B!0@!Ba$B!I B!@@!B$B!$ seB!-SwD!B߇ !Ba$B!I B!@@!B$B!H!B! B!B B!0@!Ba$B!I B!@@!B$B!H!B! B!B B!0@!Ba4}u3Eӽg^|Çhնy(ZS 00Ps2dNF}~)fP9UtgȰ%8yj|ˠ?e*h¹%SR1Pj yߥB_g/߸!!!L8Çp-Z뀟4mن?2mڴn+1kf;e5z֭&e,Q B2f+iG|ebaUzèÐ7vp\/Pq O"&&v |cx:`J9~!|t>s/si޾@mzρ?yڦJQm9D$m"8[CP(`obb8qfւe +ZAmV7};,Bw[j˜L9}W"##Yd(Oiݮ#Żj>vF iLI-Zfيt҉…mș#E `ff.|w7WN<#Giݲlbjbl~5Wc5UAEu׽<_ƌR e5cUDDDblEm._I:X^r4O>+tc儆evZQc 5yѣ8p0NΝSfjj<߾ؙ3gR{_RBCxi8ŋ`Z@Uqtv}pjU'O4?_~L˗)dJJu6/[6 %&&/(SȦ6.ԙ_ZEVV˛+W<''~ټkx_sfeqq#>WAuS;k*9L/`|uN7yлى+ ?J/q݅ҹ:YKm9N>k&06PҊB|o=+Wx\L:lŋ3j0vڃW%hET֬[n"k֬xd$$$pj;ȑUv+BCCz]^H;cێt w7W̛ͮ[uf`ʖO.K,lZmѢ? ri߉7o}ppv_Qc`-7curvmBl/}K:^*OyB,kob%?-B^l%2:~>ImӫZ?oƴ)Ф,ëw.ħɚ £Sp&SNJ5pL{`L'n:&||!KԠ.P)ַlќzu?7O.\>%նeuZ4j@,_-h~?wgϞSS0ablLHHH}?{{|tgj=|f>5U,s,/nĎE|<+@Vm8qPVVy pE>rh6Q#Ѩa}r=ݤ;Yp/)ϗ/ooāI Zl%~,5_=dAOOWA(\c, ߗr.ZB7^lXZ |RRz. L'̩ m 3.z>c,49'6B!G>ص{Sd)Ԙ`_8̜3ռSM޻'OfBLL,cM3rTswwc|~,!!xuKVV;T קG̔t s/kjiN>gD]ѻgwq NY*_###A>`˶ZƴUfWWx2 M2癿Ȼ٬@<0$ %Ars1 ;+Jj:¼ߔ<`q0ԃ^o#"}[ w~ٲm;[Oyhei3'e~ȝ+իyӭkw[/^WXHi)3Bm_:J Jd_8kV`b _ _սޕ7nF\ЮM+V^]ڵ~ -ХSҴd<~ҎV">>RJe?@LL آOd5"@[N<|? FOOm[ӭK\}{E0gFGW_TXAt$OkV.ct덡!e\86Y:wlOXXk֭gͺT(__\xY̙9#G3nBٲѳG7z7o[ffflX'31*YKoj}ڵ!,,L5gYw76icUؼ,\kΝҮM49$=c*Sgb⥼|J6ۊ%0i2m",7;bTP2)i@-72tpK2f=`e +Ñ4_1Jp"X(3 b&pR[e )&,d!C'swËꨦf#Fc8*SBջ/jOŊ5$B!H B! !B!S$B!$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B$B!B I B!@@!B-$B!ZH!B!>  !B]!9B! !B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B5x2-Z+no O>UfڧxM>4y*n]ԵC~zZ(ll \{!=aI/2/VB5WnߡET*Sy53fϡSlezzz<} 333 }O*GnM ϥsbff~}޾E̵3YͲf)}7φ+@wC`q.tc]n!_[lي)sgMG__9 :w_-ZP_TH;v*|WqqqU𛐠&ё^bzsvI-2Cp;첧>.>K!t׭ wPu2gHղϞ;wn"""?B%Ya={ӠI3e88PƽGuvGf?-p1vn^0=&L"..g7 t?P:oV]vxUfμ$$$pmllٶc'7n^un 4{juzAA)T'WF@lljZ@hӾ{й+n-OyEmZ=>$66PbU۠1G>ݧk^8Sɕms-m?AM/W%of̚:w֞UAkiGJ.Weo6mޢVF\\3gëK:Ѻ]Z߳O4iƠ?S+WvEJpsԩۀϝWkφڗ_ *_# *CNAI;nCEe$m7IWO=J|UY?xXB-055<+qqqy?_gbJ;:RTIvO={Rҩ n^l޲(xug߁e?qZ>5%-aɮ={ԥ;_tUAk)W΃޵3+W3b6 O/?6_àXt9ҴIcV._B3oR`F)T {vnfjiMdʔ'иa}Vbμ?&&:S'3t N>M7?;a,\6[2w K8~dGDDҴE+BCC6esg@WW[bX4-l 9{ZY/G._ ϲe4gXd];ud)Ѻm޼I]|L0^4kڄ㈎W|賡_6oޞScmծH[L<8{^p~5y 9`Wk]N܇.[?tHzU.Y H-,Y dOC(Z%K8h\NF]y1Jdd$>51k6V,[9nבG]gIhۢExoXI.(\؆9rP*WWJ;*wy\9qu-)FFF-׬^qG*רFDd$+Wѣ[չ~_3ʟJ+`Ռ+Wq/67of=~JJм%SS]:c!-ݧW1eڽ*u`ͪھR˗biaTNFQ͛UM:SiGߥ<:+5kTS/牕%ENLJTRBC>8}q^UXGg7;NثW|5 ===L IgpwO<US<{+K4 Z_uZ9sCpRE|<+@Vm8q?077'22oߒ)Sӓvfnbs/p-݅7Xnuw۷;~Qy_/I>ϟ?Wq,vwS>^܉o Č.nBdʏԲOr#BK}Hdn[@AeAB H\bbbp"Eh^wۻ 1172.zR~,!!xM|v++ͫW(T ۶sYda`l~Ӻe ܹ+x7emٲه1&]2O>exzwq'Kv++VǏ1m2]ѻgwq NYөX[kgCϝ.=#/(?+4vH{[2eh͢&{^}161SK7f$3ge9<bEYx |R9߃ھ>54r=5{׽+7nb]VZFmkq ,ZڵKᎯOM/\'Op*H^~`E*hP.UTf```-qIQ# uÇ`i߶5ݺ$x"3fO *Uz:'5A+1qTА2.NLѬC:y"ӦbE]1/YAk!I'A<_C誈ܽ|jx  !B]!9B! !B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B=xۀ+WX ڴeѣg=Wë67!龎4y*n]ԵC~z]-FW%4*յ3,?O6o_h(0V^I]9[-5Bd*8,,QcƳ{>bccO?T/E-3Eėϡ\K]"[ s/F‚f@LY`.FFFXZXк]G.\GYwKLߑy*͛fti;dt/!qqqet5wj% 6`ꡧ dtk|mP|lB{ۯ,ۢEXf'sLnc+۶V66x*<Ͳ\I5*Ε+M%8$K~&{AfWA6qpt{9"4͏&~T[6bU<7a=z'WMD\\n @2emwMNݧC8:Z<ŋiM||յR=zUI'iܴK:ŀz>ƾjuu-[IڽbOz=v\.h:llYl.tXf̚CUspnj cb|HYd( x#A7+Gk@Uelocmlm(H>%5'MuOH 4 MGrMN|!:.i7tr.9'A2R'IUb)~\_מ&*ٰq? EkJz?Ac)RQ< Hp [m.v2ESܹ=w8NuwpڴjUW;T-S wyKttt15}zk^:ujԧXru2Ct>K`ނEԩԀd37g<9z,>5k`,ʺ׫/Gg,[V.˴[ۢ,[?<|DǨ\&OYx2!!Um;vҳOyz2ls7?_ޮ71h@:o OCpRڴn3ȗ7/:w_1J'<2>Cobb8qfւe +ZWꭆR9aS:mQ@nRoo Z:JlM\R:0 yo7$ ? Kul[o]7ad[~vF iLI-Zfيt҉…mș#E ۨxzzBiG邻+'N𑣴nقbE151ȈbE?xի1nHՈd zt:}/cFSb̲b*"""16΢}ll, /A:nP.]1bbb3o[Ȁ~}X{fSCΝ[T*Uv/YHr*1Ϙ2me ]}?_>J8ؓĀ)dσգ}B =&mW.?5Qα쿭C7w,mtX>ޖ)8&{<򰒂G%mBrwT3QPC˷2"!&%`M͢CyZ/9<85\`7#\MwӒJFp/ Ri͇o9oTJ`{K M K0dR&44-[:Qߋռ?GSƩ#ֹspиZǸ|9O,-9}/ O\k֭ӧ䱶NuʗjJT\)}/S<ȞFp-S9%`QnԏsL:j$u?W_2P *O9Mz7`-%(v|u6*<YwTU@*#FklްcTYn=ԯ['&k֬xdɡGI#Gxx89rf݊Я^/ Iˏ~lI$Ȓ% 1s\~ׯ)bW,(/Ȟe' Sr--,Ԗ[ZXXܽ{iGTf̙;v1mƬ4*ijlYݻOwiA!7 ~9r=3$%YdIsJ Z*DNk.C7%':rwV0 }]h+I+ e|@];XM1]֚JnȪJ'"3ܩ$k%Ț/ҡE@ !AIVy/#WQj#T,Rc.x>Mwʬ@J0+ۡ5L%Hte !E `~ټ*T0mbccY&%KO;7PQy={OM%-HOOccBBBRs>K0~b-ZđC)R;L>SSkO;-۶cbb輻E-100F5o8HDD$ xqeqO_Z͉۷dʔI?\' wc-(=d?F )71ەt}m?.2 Nǂgaj$e܍Uv6bKA2)HBwyϫNݫ1|7AXnS{MwR5>@Y׸-*_9Sr=~T l=L>ӦvӧyFW9UUe̸ qq&WΜun?pP5%8$nʊ'OG%v WlɕK9λW-(oܼVFoG5kVUZTV<~y6oFu:@WWg֕0>zxֹ3j_`&bgI1nV|" |*S;[mW N <|SiGիM!lIgdjwڃ}!Kԛ"r Żao~bJhF>:ҲtRxF&mqf$x}y&-KHPp$l>WiL>6^SuS %sBV#:%Y68Le d{>4ޠ<_"|ĀO%NjL1]]|}jPbӑ"[TO)9kccc/]ΊXYZR^z6}.",튱|B1H;S6U:+AI99Q`vު,THIICqi_ZŔ+Mה)1JbJШʝYp+'%0ҩ4h a*Ӭ2W m2CX}!?P "_AoOM9/9M'3#al{*ANmS3)z'jZD)3V02K}jS!N$aA]!_Q[0O6]^wS !ǫWxTXQB!P fAoa?}PPGoBH B|G`mS{DGIjB!ߩL CB!ߞB!Bh! B!B!B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B$B!B I B!tg]W!Bt< 7q ? B!B !B!B!Bh! B!B!B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B$B!B I B!@@!B-$B!ZH!B!P/]Errͳ} ӧ͖۱O:}fFW3iT<2";J] t%Cf/8:}J_~vBqUwhѪ-3sT^~͌sԥ;[ـF<} 333 v {~߽61d??o{t߷:vl!> P.?%t.= =@O ! e+̝5}}0&&&tܕöh%(?Mh%8U*DzrvI-2Cp;^q}VB!2RGօw:3gMd<ܹscK!!-^6-޳7 4SzZi e1_Ynn~4iڲ czŸ ѳ.8z0n$pv`0J)KHHH`ي@k̙n޺=vƍګۮ{Y.vԮcx 5l})g*y3cbccSߗ'h:ؕSv%J[;v?NVm/L UXdG"MNi w֞i3fQ.R~jDL6wO]շ?aB &׈d)=nCEe$m7I뛭 feٿ&q2~ *F@EpE={vx:GA)0J"_]g%..7oL BiGGJ*6<~.t*l;pP,::'NQ˧&Iм%:::̘@>=ٵg/t'> ZKr]HY qm}zi  M[А3)Ms4oڄIǦCmh[:|0llٺ~>xx 1p!l 9`Wk]N܇.[!<} KB͢Iˇ^rVZl"1+Ogtt  BU.FꭆR9aS!_W%Z|%P ~6BGG瓶1008K+-B݆ԮѼؘg~%22_̘5+KK-^<[#GZ峏ݤQCZ4k m";1*QJ K1Rry"Vm;PbU8z8a^ϝwy5yETT1118;sUKDWWpޕcFi|>'5 ===L IgpwO<z9Vinf>5U,s,@ځ@,_#݉Ԟtuwo߾ͣG|F8C̙[ۗi:wYmxի/>N6`_&IB_q~ųk^v.1UxrplR!&F8c;<}m3Q;pGs- (6$$л$5&&& .b[HFwbbb3ne\.Uݍ;YCB(jy٭xD='\xzGrdfN\u%&܆R{͍7ՅdĄ>©Uئjͯ2eFuo׫˵?H>8p-[q,URUc(##T O-{'>ֹ3jLRB$w鱒S]IO>P%0LI rּl׼pgT>m{,*RFk3@{hYZmH!ץ7rȑQu\,]7obfjʵ?`ϗ}{6Yd ZKlxĀuO "[F!F\vNQڱj]6,YK`jjʹ 6|$%ؿ/::::}?.:ú  #:* lfUף.KXn~ݻ ˗alٶ,Y`hhH\x!6mG5hcs#_aeiILF̜=ׯ_Ww/^|9OFE1k<^ Ѐs/xrʺNss5=F14T<}u6Ҩa}C`W^ĄfԘqleQlu>[Ę?aʴ=;A|UAk[6 yXꢣ0fN̙ٱs.ؘm[gGZ|'E`k p'ޅ;;sꀑI_ҡKJ}`Yy0gSCmG+ƛXuނzSN*O !^TrW{7*?`mn@)#X_ϞųlY&]&T*GXXVa݆Md22"`8JvꥏطǏX-<=rQ-_A).lC||<[mg]2W\U {W'̞7nҲE3Svyݿظi3TR2.;~ʕ*Rr%߿OF ƦgʆMxq;J;+WN~ٲp"ujҧ-Re6V TʈCUw9$ȕ+'e9rV#-c011 8|JbeeEBB'SiGYd)~قep Jb p]V^JNwyV?[{]YaX%%R!6K;82wXyPR͔l,:,]@.Sj;oSJ'I XH fs(3 3(ԷD!Mz: _#!Zd7/׫2*B!"z7ŋ5ާbŊmӇ !B!?!B$?]!B%OB!B!B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B$B!B I B!@@!B-$B!ZH_ ft]B!EK󌮂ߜƁ!BCRB!B!B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B\~6;ArهGM]DD$f̢OmJG.ܤSqvjAft(ll \h]kOAgXK8 m6?aVJמ*O>3:mB[ ?iԴ L?Æp5ZHTTjW^ѤY Z*L4++ˌn7l֞W^}~}·ng]Tɵo\v_Z@OS_;?LkS^5۾|pϗ-!kO ̔ bdduKxu`ʴu+Q sC^߀ptqŝC5w>]{ŝRNlӞ7o{j>t Jis 5l})g*y3cbccUccc1k+V ~[{N: ~PYd拴H*O;I Oy_v4^>< z< Hp#!Nӹce_@__\|osWJx׬ũg>KS[C0cj}dמtUAk)W΃޵3+W3b6 O/?6_àXt9ҴIcV._B3oR`F)T {vnfjl۱}SӓsgcWqsaօqcFIՏ:k1mD?hpE[qc?22-۶0q,V,EOO:"BumZlK?f$ÆCiwDD$M["44iS&1w tuuЩ+QQQ :e?lA+Z[bX4-l 9{jc⥴iݒg/o^:t'?볒ֵ9CL<8{^p~ M T\ 'Z!<|^q3#(%Sߧzzjž6P0.?BtK z_XXLy&XYY[}{={v,ZLn~=٭2}4R#F:"ccNH|}j0cl,-Yxȟ:rQVc7iԐ͚`[Ͳ+֥ ې3G OO\]](<]pwsS>r-[P̶(&Q̶(h\ ,ǟ1m2V8ysfRJu6nvmZܾs=;a[&KOK ɓSdIe3҂q J;:ܢΝ[}vFFL(_Γ}P-[3nH7*+CLNY144Lھ9b&J[GGy< ?z% U- 0CJ :H 6׷7uU*+.[+[L!|CcccwF8jw1KͧR" ` uP7 =m8tҎX ӿRF5UP'V>;sLj+W@hhO>MsxL(_*n=v (s.O>F5o"&&gg\mb'PuUrRz!!!<ٓ?3eV;V1ۢ\~ݵ8C…)?g'\6169[>n4m8aav,U]]] ©gƯ{WƏ3S:ujѡ~:y:aa8]78'W3]צ/Ws݅ҹ Xfbp/͎Ѵ ¶C*n*)C&{ /dtk !7y"0bX\ k11VBGGIḻ/n;w3m45kV{wj#t餤ENww擳nEhhW ۶c'O/?~Y9f,!/,78X7 ź9>obB~y$>;/bЏwhݶ חB Ǵj&X+1o":tꊞ>ubP~dfNHKdɒ%iaak9U&)7k.C7xf)ehvE<lSIJ{{JPcp-%BS,X_6o%(p9 T[=Gvx9(i h~?wgϞSSI 8ջϟ==tttH;Ϟ?ʿ/h:|}j׽jYY,Y0zpړ%9J)5`_#Sl/_ޏ ܸy{w~>wGYw7ʺ[?7ytx#§}n9o wex*feA{%=(<l-N񉊅`Nmh,XB!2^ڽg2sUEreeݪea\qbŊft|o*2fʸ8+gN:ww78GYTneœ'=GlɕK9W9yP ܸyS]]][vl151ះp*z)ʵOOrIm_}p-S7OrIvҧ+I +xq!F oo?6iN=h٢9!LkTjyyfzF/^b57[ىc'N}.룚w:z yTڑJX"66䱶fܽE$us{>I%)T}z1nB TEHK͘Ei`tm_CCCߥ317JF h֮ѕVV?xMlXZX|..gߠnق;wtJ"")H[Yώ000J))[֝=0fDVӧlٺOK;F+ *ȶ;y/Yjݬ+f122"&:WDƻL|%L:BceY'W\H&W|Ul k&>k*4W5?Bn}f˶lٶ]m%gϜ`ؐ0ΒeW+Jvdeft|ھ>54jTƯ{Woĺqs-C6Xzvuk,X:kᎯOM/\'Op*H^~`E*hP.UTf```-qIQ#(TڴؘK"p5VԯW={|5d̸<~ۢE0QkoffƆ?q2GK%XtGP/7&1dp-‚4y*+s ׇ9pTՙ0zZͳRݻ*? LL?uDMż {Eqb,_3M:!ð}6̙mؠGIdqǀc֦@ ]p`bG;B=yR!XDX)}]hAɜvC3]91 GR8BIDt]p~: <(]Z#Fs95LB!z{EXF}CB! B!B$B!wJRB!_B!Bh! B!B!B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B$B!B I B!tg]W!Bt< 7q ? B!B !B!B!Bh! B!B!B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B$B!B $$$$dt%B!)z133h;y" B!@@!B-$B!Z(]oЦ}'<(^=ѿ^OU.^LVm)䊛gȓO3IOyU;]!ŃP|r뭆N[2W9[aIr89~ 0+_7MM!Ғn?ӫe+133e`iaAvpeq,U2~GիpZ4o /[bjjYWkbbB]=l6߉1e҄82"՗[p.(ug}( j%bʌ=IB|דu\oZի4[r%ռ;W.ua sLCB(Z$7lR[޽go4izlGʸc(_NhGe.QËq&ѣg\=qr`܄I'%n @2e ,[wZ98Uٛ9[gێܸq[{s۵{5k8zj]u98oAVms~z `W4UsW?cO`ck[U+Q~#JƷn~?w9kgJ9cv\Gu޳7UеG/<*օ|I?⎣;'""3ŤS?p0\qtvcb2 wO]՗/C5$ڱs7ucHŪYt9g٧? 4c)^҉>Y TrêK)5^;rdݷCt\{]OS+M*'.S7Ð7@Y鱲4&&J hc{\@VgB8 g(ec*59D!o:0T/CS&|EDk~N *m3Rm6h}vAl';c`:\>>݅+Nӹce_@__ҩsN:CV-ULMM17J\\o޼י0ҎJ·fiaGYw%KS (M?4o3зOOvK.݉ ZKr]HY qm}zi  5dCfX:ì30ȧs]-5}aaaL2M`ee6W¶h\S[|%P ~6BGG'M]Z>L9}W"##YdΡu>rU>M5E3%ʶheVn߹˞Tl2_([ޜW^ gp_93`ԩ\,mVXZXx\UJۨ~Vķ@CS~>NۖAM@GG~m8zC5dWOMLL; -a򑧡4sBh y2,18ͅˏSP0x%P;oB'JJP8:/8!oO?m-+^Rٿϕ@,ɹ89-^ãGb;7W.e:}" S >U8+=+׮P/5E\r9spm^Sv-mԫKLL _P-Ȧ6.s>+wE$9RO*[ 'c쫩 QM.ɧʬ<=z6}rPm9 /h~)_?0SLc8Eyio.>sm+>i֦紻B{}'#FklްcTYn=ˑ#;9rd 4lҜOPrj;YUޓP˷&K'%-*..pr3Uv+BCCz]^VM߶;?p0}zg1c I{jĎs~Rˑ#gkN9>c-X%9z,vlӏս5.Ep0ܡxIB5|I'+cQ fϮ<{gE|}/y&(')OyCjHWWgeh FVI5nϝIuVxd}ޗ&c^0am;̜Tgoɹ}: !{ `~ټ*T0mbccY&%KuGGWW ^IvŊ l:u}}5ϝٳT҂016NgpwO<s)+YO wU-˜9 v.9\;Fv66}ؿ/?`%tņ5~*`lb]|yy%$3E64ϊ£`ux |ߚ_w֟;!n~tN=ProQ$WФ P@ Q.S3t.P|9}Hy0Ijɹ}: !kjЮ{:}&3MQAf}ޏ}r>L!p^QO+O>\(_hTM2R??I<ȟ5]I49/B /֮E6) pt,z|**V`)ics~ټ}Ѽ3mlJ,f':#Pbİjatڃ-LiqJJW'k֭gxq%V^{oڼkkk\8v$wb@>.O?7s9na~]YgௗUPfY;<^NmзnIDATDcEèIQfr֦IS*}i%͖mٲm:+KKΞQ梻x2/_ac_8A˙:}&~blbLE// ھ>54jTƯ{Woĺqs-C6Xzvuk,X:kᎯOM/\'Op*H^~`E*hP.UTf```-qIQ#(TڴؘK"p5VԯW={dt.+,b¤CTTEp슥Zf̙دs-T\3336]?XJ,8~l CCC.^J5Xiצ"ϣWl8y)/T@Jgh*S.< Abʱ~Kvg9%Y^o90s֖J:n/TQU۳N+,2;RLf32=gm:ef%>f -)=TP ٤HTT? Iڪ鏝ۗQ]t?gyxQ^UH>kC=w`f;[~B!>ݫϘ/qfG@!p_%%,rG8>k&k@@!зJP&k&k !B!SMB!"B!ZH!B!B!Bh! B!B!B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B5YphFU!"]4*iB!?$5H!B-$B!ZH!B!B!Bh! B!B!B$B!B I B!@@!B-$B!ZH!B!B!Bh! B!B!B$B!B I B!@@!B-_~r5tutqwseAcݫWغm6nb1*YBm?hp{tB3 I~&.=UB|G=逵T.cAl= s3"nHA6BOn?>>ղ>{C P[no_<ﻷ~& ҹ_133xqmvoߒѧ]OUZfp(p1\zl!"Z FU2B|-X233S/nב /Q֝Ϟ7z$t5ղ>{FhCn/!.P.Efhn]]ƫl?WPhFL!_v:W.ڷm ll U8E~*5O̙,έ։6SB%Ya={ӠI3e88PƽGuvGf?-p1vn^0=&L"..g7 t?Ļf-͜y HHHڳmNnܸڹڽbOz=v\.h:ٷ |j9w^NM+Q}عk5K Gw]}iGJpH-Y˗/0v4M5"h:6lTHhEhh(ӦLbСSW[6{vnҒug6[bX4-l 9{n:}*xx 1p!c?%r0]s),=Äp۝zzjž6P0.?Q/g~hC1+@sl `qXUu4TX !> T VCG)6XsYK]6r)z .] ')i޴ VVVYЧ,_ȀPhvڵ|1s̯DFFSfceiɲ 000@nבG]gIhP52pb\z+W0qN &ϻNڶnSiU¹ R eVn߹˞۰-ZޝUY rل "h" f֨hN$7TEܲ17H ̭Ms&q (?\ET@ G׋Ͻs{077'| ݘ>u A!Z#i{bbbB>z'cAhղ%j9H/ӹs3s3R'N|NcfTN/))(j?c&&걫-|57QlP{E[aV/foqhwiq`?˵E0[mwn\mX^ l vTpn x_=8prPB]ˇA~>DW+z/=w?òdX߿ʾBQ$c,,1̯OG~~>&vy.E J=gr!z fx­ hq^_tɉc'+(Rյ$l멩xq-Hڷׇ+dee{v6<υIMAPrrriÙ4:V.{z]U&22J.q4M73iFgΞ g"Əe^t :+>x}Ѣg h4xzz|*?TtiCwx^ZbjjJDd |vm}{{c)y݋݁y 0VUT3*`+yjpmU3E`w;ul ` |RwrTڏ!6`iVtw°m-9`ErU#տuUUvЩ>ۥa}S{>^AGi=|U}Ч9n LM*y>AmAcr˫<;Ks2! Xߺ?RMaJq.ˬ0 }nUs BQU*=Xz-mbiذA_3=cNЭ+5k4gjԃܸFpdffFSwڍ4:؞~Z7Y>NLHpѬt͚kkWx֌iFѝXтY3߷^5b151!=#-[W^.9?ysI/K~Ä߫~%;;0},] k+Dj b㍧_)J*9'bO^j gRXTRq]uj]p6V5Z{eo.6R#RO,^eKS;,?1#Ơ:Nԩh֤IvNiڵ|IܛpķgdСo9k\n\Npo(++qbbj9oRlTdf1Ò˹1555ol3lmlnjܨ>kۖ+WR\+W|*F3aCr3J1엿LML7;;MU|f/a"(0~}p%W84NԚ&1U`8,YBҋRwGajaLQ>vM 35VZem-u3x<~B$;~8Mxdꉣc-~_ ZVK޽1k..'l&Mogkc wfo>N17nL_ě#G1d 3҉Yvm}xkџ͟0s<:rlO]vOo>®/$bX s6'Щ?ޭBgmqF/ rsW/hղ% g0Ώ!=^x3X ֗Ǜ6ᄍVKb>~qrtdZ,}5srؾcs-(v>>h4֌!p"b?ߊJֺ!98{V`_Dc]l݄Xfύ&[W}.;t>O)`SP-bÀz ` U2GSUǪkU:P>51Ղ>өր[!=,M.eG]M0+t펺ѭ*YӘKsAd_;:iIJRtPW!ޓHnn.wb]FNN|{pۚ;{>X{˖v&6k5hܸQۨ BIڷ_TȣE 6FcggGHpjA=O>O>ŷ][OαjZ,,,1!=kH~^ w/ۋ]ٓN ȡGťa FaͺX6īR}{3QڏvvvlϼL>]^Z>Gܺ589:}.&cjU‚L2/SDz|r9s/`i4mڄU+gFi]ƼCDd"'MaǶOeⅼ;-CY(:8(1|f- pp'0'}Ἠm7[w~Z YXgU  L2P ʖ>W1Dv5m0LSeOu1ԂeC.Q}=_#1zHk*]!x"']<|;t_LG_BTIp%!xݹSꅅ%R!BQ1ńBǑBQ1$5H!B'TԠGЉ !B)@@!B! @!B!B!%CB!J!B!! B!$B!@@!BjH !BQ !B!! B!$B!@@!BjH!B!! B!$B!@@!BjH!B!A;c%tEXtdate:create2018-04-12T17:59:45+02:00;}%tEXtdate:modify2018-04-12T17:59:45+02:00JLIENDB`django-tables2-2.1.1/docs/img/semantic.png0000644000175000017500000015530413542204315017675 0ustar josephjosephPNG  IHDRI VgAMA a cHRMz&u0`:pQ<bKGD oFFs'FDtIME ;099 vpAg4lqIDATxy\OW{%EdlR!b2%ha,|!;aA Z֐"YJhtշ"K{9s{Υ(B!iuB!y$8B!BCHp.B!\!B !B!Bs!B!4B!Bh ΅B! !B!^88=z4̚5 JСCqtt$$$W͛5#G|bWbmm5)))/oSN4nWWW֭[GnnK?O?DYvmkkk6nLQToҨQ#Zj7/Me|vޭݻWdƍcmmͯ6aݻwYw kժe]%_!x1/}<**GȟY{)FmYW=x}vZ177'**ooӦM _f?2bf͚۷Y&YYYŋ_Iߖ\.]?[u!|/=8[.ڵԔ^zu^gϖu^Ȝ9sy&;vG=*i&^O8p`/ˑ#G033ߟBBB2e vvv֭߱[$%%ڮ韭o Bh>ݗ^.ׯ/v4< ..##s;x 9tԉ5kMpp0666/x'Ndĉ~ ёڵkχ~XYڮ^ZvMlWý@!{$&M=͚5?=*9|8;;ӨQ#:ẃ ѣM6L6VZѴiS&NHjj:]NN+nnnddd?ꊍ mڴ/ݻ%j?@ǎx葺۷y,X@Νiܸ1mڴawޡqtޝcǎ=̟?:`cc+>>>ܹscDӦM߿:x8tjժ@lttt lOMMaÆ4nܘK._vƍӺuk>|_Q~}{=cLf̘Qy\NNNƍOh&NH֭iܸ19rk/44>Mҭ[7.^HRRǏ'''O^WTlڴ h֬&M*vښzIƍԩS9V P՝;w?[>dܹ`ccC3gNsTڞVo[Nƍqtto!--P͜9[ҡC1ck.vJӦMݻwNiҤ ۷/Z}Kr 3x`50j(ڼuM4~l۶M'996m`mmz̙3iѢر~G>/KrB# 4pY&[nER=5oTTddd`bb99 6NN߾}‚[nŋ={6>>>QvmAz!CV7>>[]|y*U֦QFT\\Kdd$FFFdgg3a„e:u j׮6aaa1={P^"СC FOO ܹw}GRR'OѣG 4۷oSreUӧ3f 'Ncccy˗/-|\ hժ'Oȑ#4mIff&ǏsYq(?Ύ۷;C=۷/m۶EKKKۗDVϟgĈ민=OOO X"?*Upu6n܈(̜9iӦ닮.j">>;w͖-[ {֭^իcddDTT}~~~4jԈ *P|yөPj@OO!C-j׮Yr%L:ϗi3g+WDKK5kr}֭[Ǖ+WشiS/7o&##KKKYf TZ}}}.^|?5kTݽ{7/_ө\2=z(%,?>x G&''333 aϞ=:nIq1o<ΝK׮]X" , !!ݻLנ(|Rzun޼ɗ_~ѣG?]6?x{{w^_KrB6nܨXYY)͛7W\(*obee|-[TOREQNR>uݻ+VVVJ޽w*(SkkkJnnbccXYY)(JjjҾ}{J SEQ|||+++EmW+++e̘1%jw``bee4mڴо?SYjCEQbkk*(sU&M(ǎSEQJ:u(XYY)VVVʃEQUV)VVVrmEQJ֭ *qqqÇ+++CJFF(r %886* R=zfŊ2tP~) 6TӧmVR.](|NJ2cƌcITv\pA?TS@@beet֭P +*J2dbeeԯ__]-[+++F}-_pA7owUlmm+++(emذAQEPzXYY)}. ,P#Fj~֭[XYY)vvvʹs}e%99P]v*))sZrz+++W^^WŋR^=QEQ=z_R[6l>|XQE9}<|PquuU%Kkee((ʰa+++{\5888(VVVʂ gϞXYY)_uUk3++K֭tE^zJ͕xuY%g̘XYY)۷Wnݺ(,ZHoWVEQ""" (VVVȋoIcB!Zv @޽ O?{Qz Bi}W͍BQnݺz4NQǿdCTGߟbӫW/>:y bbb m߾=m۶T=ɓŖ߆CG"kժE&((H#88KKBSX?p1ԯ_!!!䐚ٳgqqqB =z7n9M4y|y_GgC o|yر# 6իSΝ144D[[[} LG*333F?3uݫW611aРA7o>W=~7o\7L{ܗ+W!CyӮWR%unCCCH}n6l7G>ʕ+O|E|ɓ'գƍS?裏Jzmߣ/ODV.Y-Zio߾@ԯ_u뾴-}L!xZK!22R|8-o߾ԩSxRSSiѢC~}4hPyULL VVV/ԁ/^d߾}\r﫷?>($$$&y7o^N>[.|/_P7tR5kV4h@Zs.]"&&:v숾>͛?~)-ϫ/4iGeŊ>}[oQdNo200PKOOW;&&ݻwsyԋ#v}W\&}I$?>=/[TgX[[ڟ/xReddy?ESbb>g9sVݽ{f˖-ׯ\|͛J|OK$_X,bGk.4g0amAHH .={k>zh :lذSNq5 /c߾}=zSNӖ===:uDիaaaܺu@mll ommm^7ofƌ/$E?V111؟^(xLMM,3MCix|ᮮnѷ,а/UT)6R꘿e˖Xq3}}bg9i!w.0Pg6nJHH 6J*8qٳgpBloIcB!xι#;@HJ:{t##"aԃ;v,]tO>1ŋG9`ǎ? ))),[ ț[?mIddd@bb"qvvvT^].=""B i*_gϞUѣG,]T=/F:WT 3g0zhy뭷={vm۶rQSWUFƍ9~8qqq8::9$LBǎ W>s:ܹ3zzzt{Ou yݽ{JEŊ1bܸq㩯{111T'; _Q>| 3>C~S> ,4H{聎^ͯZ2GZZ?z˗/W/r+gkEԩS`t%7<|8[n֭[tЁjժE5 y9rse\rFҹsg~w :ٹs'Ϗ`yjժE\\1eʔAz,֭[?䄶6<ܜBDGGL5&''+++W=<<سgGk׮Ԯ]tקUV?~ O?_+FկՏ۷:ҢE>044Y(<>cǎ,Yg_YR8qw2b*WLŊs|9؟9Ǐ'(('''j֬Ibb")))ԪUTD1668::bffFTTzAȿ°GWWLLL 8p ;v 44>}PvmݻGZZIII̙3PO>sQ>#TzC͙6mK%Ǐ~`ԨQc``۷QMyeddУGjժEBB/_c>O,?5k#F`lڴ 333̤gϞυ[ksΜ9$''^ٳgO\/Kr{ BJ[ZZzjlllF[[عGE߾}X"t҅/2n8:uĹs爌ʕ+lذ.\@bb"zzz[O>ڵks]*T@߾}K|*UbԪU ]]]066ٙ/sssRRRf֭N/8p ɔ+Wwww6oP[[+W2aԩ^ƶm۰aРA|gUTaذaw_zUرc\x+Wp._~JQիӢE v777044䯿?yG&M3gNjժܹOOO*V۷P^^^l߾s{/B B__Yf=J>cTAK__ 60h TBll,իW>cƌYz5}͛7'##44i”)SB /cƌYfܿ$Zn/s{g}M6lڴJY~5֭[ر#%}iŋپ};+W.pMO4 SSSYfM\/KrB_ZJqlxNۓ֭[iݺKƍ>}:ݺuV9y$Ą'N8I3j(ԋ#+K`mFvv6fff:PbEۇ椥q]>}z/_vB!B /58tIII4oޜrʽ ׮][l2ؘ;2rHՒxao( !B7K"B!x>!B!K'B!Bs!B!4B!Bh ΅B! !B!$8B!BCHp.B!\!B !B!Bs!B!4B!Bh ΅B! !B!$8B!BC>KGq}- !Bӣr8_zzeN||ժU-j!!..sje] !.;;DLMM)_|xZ˃0ZUs!B!J@OOUyA88ʒ\!BgGVVVӗ88WEQʺ}B!BV%!B!$8B!BCHp.B!\!B !B!Bs!B!4B!Bh ΅B! !B!$8B!BCHp.B!\!B !B!Bs!B!4ni|V _lB&f|ѳ$Wor:77jtjbV}Q6E.N&I˧ \IcKw`KnBh$M`?"GyeC:66]uoR ΋cÔHaxΙ&[U=azc}z:C!^\Ƒ$jW7Kt8-ǣDӚƥ^Ǣ=Vxەu!+uupf#cLfʺO4J-S%u&#aIthdʗ#ElLCW30r):BQ^yp`K-'w6^ȟ2}M7;9̮I.R>+p9 }]m(KS҂0-ՒQ GV|=mz=@fUvfXs\@+J|ْJFe~ !p{۵Ձy{:|KwRB+0X{<#;1TOԮbOCl9vYF0ҵ./$A5&|ybemIZYU~6"&XU5bWHlLIH$N*?ՇoD +FQ̨T4bDS#t𨬫qT*,9/Q-Ī÷{kflW+w@h[NDո4>qr9]kfMMc˩g.$п:JL ^],x*IJ+.5#6_jC>xC;T*ѱ`X[3ڄNe[f F PKԵD'gezx:65Usq,9p+d塛jҏaR^ݭ'r5! 3}Mu:UŽໄ*6+ǃtKTRy=N]ϵ؇Ԩd@ȍtmVyOGeXr ٘+BzʢWq/- ݭjWlQ\sOjLjW@OGC]Be!g&ji*sQ)xANz*W^e]Cu5vCKx:y7\)0Q*=B?&Ѫ >Ȕ\Qft^ـ=ļRѣqԨdfQ9߱əh?+ tFF_W[{ϩ:sw]`h"o^˺;/̃sX7MG oW2ĶVun9R\&mL 6U+0oϵlGM;zP\ !VUYtGn]`_ӽ w_{?/`nby}okad_[,x Y[)tlb,-&(?W͈IL9 Jjz*?sYuѮQELL':95JdB$Zp^J~z6o7B9=NE>`h?x5M6aѼ"WQt,JoµuS;NQS0ds7%.%hjaL-rŦYj `cZ(x ^GY*omWYU.{Xyz2҂~\{2UO?' ĤH2UjU315nͪGp,A$4VOBޡ6 y ]FCFuaT0e^3/on40f u]jGb\N癧 i_r:p+IOMoW'oO5FyE)7nP+>>jժxAB!*..sysz2GT }6VVV%JRB!ij?WݺBP!Bcf͚_HHH`ܹFitp=kO:ž}+8/BcdKfN.Kjy%R9yej[R֒ˠAY&|3q:::|嗔+WSSS Y%::y=kcbbX}3g,& fuP*zPjX_ߣԂsmmm.]… ڵ7bի4nܘ[nQkҥKӧ.,___:w\`矁:t7777oرcIIIaϞ= >dlmmqttxL:GGG7niii2k,lmmYf QQQX[[ŋ0`;wf888о}{Ξ=˲eptt+Wjѣ/ 74:((ܹ3V"77~rvvf߃Vncc߿?͚5#<<6߿>}`kkKN8x 3g_~2fꫯhѢmڴaƌdffi7n\ꖚرciٲ%...̛7JENN˗/Ύ{ׯ \ׄ*WAQPOp~AOb{OBe>Ts;rΌWfd\]s{l àt{O|.p+1ە~=`Ǝ^uSד T B---_h߿Ϻux^EFѬY3|}}PTa~7-ZDHHcǎeԨQ\xwMN>ݻ'22 ٳ111!44T&Ν;X[[O`` of@3uTBCC6lXIvv6AAA3?7oȒ%K?:Ů]HIIQo[~= ʕ+x{{3rHBBBXt)k׮eڵG*+VdN>~)戈k.^ȤI8q">dڴit҅P,YɓbccYh mʕ$$$pQ݋#:::>OOOqttܜ˗/HL͚5?8wnݢwl޼ӵkWttthذ!lذbjj| W^͛ŦOhѢZZZtڕ4"##L͔)SR |ڵfddDbb"ׯ_ؘ~+2ydjժ6nnn?ϡN}q<~<ˉwVVq +¼]h1G'ٶ&fѢc3"b`cCFtKhkAFUI}j["by ]9դF:̕/P_tR2VZym#.}-Wtƌ'vb턅"(ě,ewjb3ԮV=5cUT K>hvɃ}]mr;RTSrrv2IܹNzV4_jی^}  YhQl܈No9⣏>">>kRB/5G~חN:ann@__[naaaQiӦȌ38qG~by_z?ۗ7o z>z~};ohqi׮;w|꓅/Ν;}x5jg?!**۷3o>>/: cphR>Wy_ۚ=w/w>Qdѭ;s_ w}#Gc=>fͶ874_q) ؅/Ncݻ_ʕ+u^rqv܉zA!,, J?hԨќ>},grrr`ժUU###҈!""k׮111(Ȉk׮Dbb"昘k.T*.\(tPzbڴi4mڔFѣGٿ?*p~7Xd95b޽dffïZ(y̙3z5j樨( pssbŊlٲm~ʗ/۷III!** *лwo[ݻ@XX~~~%@pp0```.*TXZZҮ];T*UŰBw8qV֕V3QHJa5GnB>tyJ29J|D*heUUB@h3_N:iXØDW}&3-*yժzCr8~Ѣ:%B1J58_g$%%aooO׮]KLJ#GDǎ֖͛G,,,pqqggg3l0ƌC-駟Xd ͚5+TF&M?~144|)CYAWwCA_G F'Xs߼gEQBRD7n`eeU}mc^ZYW`Gxx3y4hЀ5kЮ]kU7!ʊsog5vZB!o*}õnݺʟo4nB!x62r.B!\!B !B!Bs!B!4B!Bh ΅B! !B!$8B!BCHp.B!\!B !B!Bs!B!4$/֤Ց{uLyk+..^O!^ !4ɍ7JV!B!$8B!BCHp.B!\!B !B!Bs!B!4B!Bh ΅B! !B!$8B!BCHp.B!\!B !B!Bs!B!4B!BhR ϝ;-Zɉ Eacc-s ӧOӠA~;;;-[V}MƸq^JYLy9|3++;xR?شim۶%99"ĿʩtD?]­OƔuSR :t(r~'uׯ3c BCC ܹs899_v={_x?썒Ν;_Ip%ժU+yLMMD__"ĿE[vNjCr%d(エ,6.&!^s'quu%<<\&""++b˸vJ<]]]|||ׯ߫2ӵkW6oL˺*Bj3CT g(OIx!xR Λ4i‡~@vv6.\`Ϟ=r ,---ڵkOouU7ṋ[rrrpqqa(ڵkҥ ӇÇY/;w.m@WWW:͛7gرgNrr28::ԩSqttޞqƑK`` f֖5k5 :,^йsg֯_۷ٳ,[ GGGXreܹ37nT9uT/_N۶mqttDQۇ+xzzrE1b8::ذ{n\]]i޼97nСC'L@VVS(;;GGG6l5֌3F}>ݫ.O?%==ݻwσtr T*w =U9u=o١ˉ|)9t{ Oqk.s /?2sr>8ZϪɥ\LrOw)߆Go35E߅T'BhW w'''yܹCFFΝ; ~:3gޞN:?FѬY3|}}PTa~7-ZDHHcǎeԨQ`YݼyӧO{nd3{lLLL ԩ?6w}6Wԯ_SʰaJTH b̘1|ܼy@,Y %88p>#3gQQQ?={pΞ=˦M^ť')9,U5#72l=W}d'f+Mc3&7b9]-O_hR"NvbTl6M8HLF#B"SN_&*ͪsR~ڌm'b8\C01dxGfoÚ÷7BJ ???Ν;nj3Y&~~~۷SN1vXNʑ#G֭[Ǟ={8s ~~~Ŏjxyy}vrrrؼy3~z  :::ұcGl\eĉ`ffСC9x`B Ӿ}{.\m2d'8::bnn˗ZFƍիZZZѥK8.uW^===,--)v{GJhԨtЁ*UPZ5 }j( 䐓1*TNF}UC[ lkW!y畽Jqƌ;={Cƍ)_ymT!%=؇MxGUxD2Ţ _~ssN*Y9SO[[Y !^?#s--==oHMM˕+Wѯ_?|}}SNyxLLuC(m{Iݺu bO6@f̘'=z?ٯo߾nK.%((͛(---V\ٳgqppx>BBBϏ~"ϝhk1NM8 BW6 .` ^T*z|͚mqnhw;yUX=%v3թDl^GGulϜhVb"yeԂ:vZ8J",,E1dضmdeee.^ȠA`s$$$˩SXv-F*>{eٹs'^^^ LJ0T*:tH=Fӧϯȹ{!''VZ'FFFCDDP;;;bbb ,P׮]#))D111a׮]T*.\]Jnܸ Ç]TĄtܹC.]SN"22>&++z&M`ggǐ!C/>B<;Ʊ{ @V5d {G(Lq2J̛;L,wg::ZT,>[\,R!ZƔUܤ)ZKL52!op+)DmxBEu,\~I&aff .1sLcӦM귳hkksuzAff&|wt֭앱ŅHLNNcƌ!!!ud5kV&M0~xƌCnn.ݽP˗/3{lR6mкukhӦ ˗/gܸqL65k̠Ae1ӧsA[t‚ 1cK,ŅCԔe˖qq*T… iѢFVVNNN|1~xF!xxxp=G_}Gfܻw>ݻwӼy'B4ZZZT{mj6V5Pƽlj{6Xмn%jWd34[0SBINƪ_n>VU;ə]5 UOi1f[u ztmVJ;H^!DR37nܐwx5L~GWԾ/ȑ#7h^z[o'```K9r$ -[, 񯠩\!ěY;5`fϞ3XYY1c ̅B!#篊!ī#\!&y8JQ!Bd !B!$8B!BCHp.B!\!B !B!Bs!B!4B!Bh ΅B! !B!$8B!BCHp.B!}qqe]ךB:rB)877V}mK !+"\!&qFʴ!B!4B!Bh ΅B! !B!$8B!BCHp.B!\!B !B!Bs!B!4B!Bh ΅B! !B!$8B!BCHp.B!\!B Qݻ޽;͚5Օ7>~|MƸq^JYLy9+ikJJ #GyOBSד43sƯȅ[)rgbyӤ0tvĖu7!4ni|LªUptt 2jժѥK!!!ŋ3|ٹs'5y^+Vȱc(_|Yv_`X*钙˦;|>Zb^UMBhR9?<666i---6mgϞ-~_IB@WU8ॗbc!v) !4L\v#GŹs.7իWiܸ1nRoŅ( k׮K.ӧO>\dYtܹ?ySP\]]9tnnn4oޜcǒž={>|8@rr2SN{{{ƍGZZ.̚5 [[[֬YCTT$$$xb CΝY~=oߞgϲl2qpp`ʕ6x`ӧӢE 3f DEE1l0lmmqqqaΜ9dee1aMV 4ĉ۷WWWŋOŋϙ:u*vvvxxxÄ h֬:u"00P]'TƎK˖-qqqa޼yTWeP*( \6vG`}~8ɌW8ĨU>8^KB8~ț*4.s~4R"}*& !4P5?g4nM֨Q#5kz[@@* WWW6loƢE aر5J=7orivލ?,XwwwfϞ :Ν;X[[O`` of@ޔ3uTBCC6lXIvv6AAA3?7oȒ%K?>_N:Ehh(_|5gϲzj*W ɓbccYhXvҒӧO/(Op<<<8}4[neժU g޽;v={CFF^^^X[[sI/lll}LG1~xNʹs6m+V|j=owٳgÃvq9ĉQrʕ$$$pQ݋#::2:(^/)9,U5#72UoMVCvNjWJxĸn}upҳT83/m}#5*ݱnY7MJ-8믿X`7nիnݺyyy}vrr m޼OOOtuuY~=Æ \]]ر#[lyc2qD 033cСSvڥ.^z 0-%ɓW^===,--$''o>LBӣ[n?ʗ_~IP}A[Y]]]  %55;;;֭zkذ!۷GWWΝ;B߾}֦gϞ$&&kFFF$&&ruix XtsI_Ns޲:R&5+Et, 5T7weD!+ׯo߾899yAӰaXf C y7]n֖Ǐ3g of͚תUWcWZbgffzjoxK}⡧AmEMyEGGSNByx l|{.+WرcĐNsStt45j@__N__ 6l2\\\pvvfԨQzEWW}}}sRs0bHOOg̘1xzz>S;(+ BsWۮv(W TJt*e޿+$؇,QR9.o?mNOO~닯/:u cbb /*``-nݺEMF`` 3fĉ=٥oO[͚5sN>>իD 8ǏsN aaaT*9t/TFFdeeW={CDDVRtFLL \v 숉Q/gddĵkHJJ"11sssLLLصk* .9exRK`` ~~~T*n޼Ɋ+P{o޽\aaa{IOOΝ;t҅:up)"##011K.̙3Dٺu+.]###͛GFF))),[Tlll8v񤦦?7JC֮]Kjj*zzz`bbz<πQtuuPBo!JB{\l{jbc.~gcS^f0ƸYaUը$Bқ2`T*_~%TZGh \\\Y}0f[.K,YfhҤ Ǐg̘1ҿ |2g&++ ///>CڴiC֭qssM6,_q1m4֬Y3 R5L>ҥK,X3Xd ... :zRXh .?k3g`zIJJ 4O?}JȈ3j( Cܹ̜9]R\9Zn+˗LJ3f䄩)ݺuȈsY:wL*U4i 4FVVNNN|Odzyf̙ܸq333,XsKWmhiiQ>ﵩgۚŦoiUu+T1gH4e$g:u͌X&[O 7*17V($88~7!BSB7ӳѥ6E!Bld ׺u{,B!^.9B!BCHp.B!\!B !B!Bs!B!4B!Bh ΅B! !B!$8B!BCHp.B!\!B !B!BYŗu}_kB=W:zܼZY/'sƍ%N+ZB! !B!$8B!BCHp.B!\!B !B!Bs!B!4B!Bh ΅B! !B!$8B!BCHp.B!\!B !B!Bs!B!4Dxzzboo[oի 쏊bȑnΝKff&>>>f߾}eomڴi7( >>>ϔ'88ϫʢcǎ,^;=z(n'III%Juܹ37n,& 8~IB9g3l CfNnYWM(<::oooHHH+V`ҥڵ TL b֭`Nhhh%K`llSY#!!ATΝ; XZZRZnF_HLL,j[Ŭ?q }і)rV 7uoR OdlmmqttԩSqttޞqƑK`` f֖5kEOXz5ڵUV|wݻw*… qrrёc9%ڵk{ѷo_.\K'Oj_4coo7wUcccM=hѢ.]",,#F`oo8nٙL}]lmmtRz2a6m+{)?--oӺukF7v <gݺu@ÇckkKǎٴi:Ͼ}puuOOO.^R? B*~gci^-*=1Ѱ$F:OZ±{}6D4]A >@3#,gox;Em1Õq1s .;ö1*yy` ]vO󜺞\~Wqw%!BgϞ%((}жm[!337n0w\7nn-bTR5jԈf͚JՕ 6oh"BBB;v,Fz͛>}ݻwOdd$ ,ݝٳgcbbBhh(NΝ;X[[O`` oV'عs'gԩ2lذ"yu9z(7ndǎرu b;vrH#ppp 88Eq E)Q<}k֬aժUZ`OyU*+VLJgҴiSlݺUVq)ԩwIhh(M6-P'ȑ#Gصk:::DFF- ÇUfٲe|';w!CqƜ9suֱj*yǏgԩ;wiӦQb(kQ hX|ҍfmX'=K޿|<SGĤ2e%&tPPy_#b2uevÞ2_o lr0g`vMvA\߹Nvlq:|ǣo#`Lzim;{m,-3prrKvoB$8}6Gfȑj KKK-[Ǝ;pvv/$,,ѣGODDe_۷oW&o޼OOOtuuY~=Æ \]]ر#[lyc2qD 033cСkfȐ!ڵ ]]]  %55;;;֭Lׁ`4Y-Ɣ,UfC uhQ+Rϼ'eݻ\cǎCzz::txj=:uΝ;jԨQZ\7">>ЬY36EQh׮lذe˖₳3ӧOFBfF\{tWSv2+ѩRԩÇ~H^J.EQ߿?\5k2x͟u޽"AתU [Pjbb}Q{n³\7ԩCݺu K.ÇL0ypOMҵY5m).zz֝{LXخ֌jM%#=:\dz01՘أ~Tl@|JQW6P+jBVR߿?-[dܹE.gfܹE'_h̬8p ǏgΝxyy4PTs!_Fӧϯ={CDDVRFFFCDD;;;bbb ,P׮]#))WFGGLnn.1`uKff&111|޽l PPھ}{CڴiC֭qssM6,_q1m4֬Y3 *1ӧsA[tRx-Z`֭|G+W^)S0ydZjE0aܹ}}}K׮] ]F ~G͛ǬYԩnnnyO'_~dff2j(bcc177gРAE.b{5 CCC<<`ʔ)ԩSKbii ?3SLu0iҤoX;w.sݝLX~=ƅ_,M>֬Y̙3_)SKxx8nnndeeT`q=latib;ծ*WyJ4Uҡvi(ϢY9fߘ_U qidJFſAׄU+ժ:}=Ӻ*7øMMCyF^)kFK)+,nܸUY'<׏bŋ͛˺*BW@SB7ӳѯUB!B'WDFqՑ{BȹB!! ΅B! !B!$8B!BCHp.B!\!B !B!Bs!B!4B!Bh ΅B! !B!$8B!BC>K⸸kMO!^ !^GW+{BܸqieZB!Bs!B!4B!Bh ΅B! !B!$8B!BCHp.B!\!B !B!Bs!B!4B!Bh ΅B! !B!$8B!BCHp.B!(ܹsxyyѢE ꫯP_h666bkk='NPg„ iӆ-[ͭ[ʺ4޴i7nK)KQ|||)Opp0ϟWEǎYx1>>>ѣ۽{7;wfƍv|M_UB㜺LAt{\3~E.J)>?&%=ΰ+$AJ-8OHH`С̟ə3g駟i_Ό3 %44sGahhH@@AAATRdAnIܹ@p%ժU{%u~{:u={u5X:m9 V͗{ҏSHZU *& !4L| GGGsss\]] Wʪiii8::W_Q|yʕ+G>} /::C~^I_^'111e]!^  PU™/|SNY7UaJ-8oҤ ~!\p={ˍ7,21SN\rrmV^{W}V^Jƍ LŅ( k׮K.ӧO>\dYtܹ?ySP\]]9tnnn4oޜcǒž={>|8ڪ$''3uTgܸqȬYe͚5@SIV^Mvhժ}99ywٽ{:]pp0.\;B~+AAAxxx`ooOΝYj߿>}`kkKN8xz߾}puuOOO.^͛7gaooONطo:_TTÇ֖;iӦoooYn666>|}bggOTT:Y|9o6vvv{\~3g/plmm3f QQQX[[>ŋ0`wozA-tKʅ|\EC?f'*GÒ<kIySe&e1zϛp) Ou,ݻ7}ɉwy;wٹsggΜ[oErr2~iYYkԨ͚5W- J+6l~cѢE0vXF͛79}4wߟH,X;gĄPNCr'00P ԯ_SʰaÊ<166ѣlܸ;vcǎ5((s1}ʕ+}}9r$!!!,]kײvBi#""믹x"&Mbĉ<|G1~xNʹs6m+VT[bC̙3|GL0w#hܸ1gΜaݺuZ@ue˖'p9 Jb,]TGHKKcÆ ?MW_yk}tBhh(K,)QT*VXgϞiӦB.RsX?jFod@*Ԇ<qݬ-gpg__/GjT6cݲnBýϏs1c j֬ԩS;SrȑB+N8AZkfȐ!ڵKW^G}9C=]bE&OLZͭ5j(/xx×\:fF3ԦI h7]%=KŭwՍݺFY7Q^٫7nرcՋttthܸ1˗G__]ұcG8Pdjժ7ƵkʺJ]nP9~8Fk֬Y }ZsK9vժU*/33e˖ҢE V\If-233#>>Zlɉ3g-/k9ƍ:`D__ 6pU\\\=z4w}b[4kLƢyq}uڒ<+vbȐ!8::x/@BN=s6eفl オ^?WS*dwjܮX"b2ٽ~Y7O(dܹ_lT 8tO,BOO~닯/:u wa_QAf~_叾ԭ[(vi dƌ8qѣGp{o߾MڵKT=zev́J48%˨(Hvعsg'vvv,] rrr7o^ST`aaA:u[mE\t+W>w{oμy1bO} fgEo687޹G>dա|,BH:t`ڵ8pJEXX-Rϛ b۶mŖ-[x" FH$11rqv܉zA!,, J?R?QFDGGsi+rn={!""UVFLL \v 숉)0G:?ϵkHJJ"11ȶEGGLnn.!6jԈ{ILL :_xx8w&;;ErǞ=zR~cEEEa``+Vd˖-hk}|>|ڵkIMMEOOLLLy?N\\YYY/(BΝqqqAQ/_Nff&999=znױ]vT kʗ/۷III!** sssLLLصk* .V#ěJq,+.^:ZؘblX*=1nVXUOB)u,\~-Z0zh<==5j䄳3gӦM귷TTkrYڶm;/eg...>x` Ƙ1chѢ?K,YfhҤ Ǐg̘18;;s |2ڵ_~[6mкukܘ3gٌ7m۶aoo_1={гgOΞ=[dZh֭[iݺ5_|SL޽{jՊѣG3p@ttFtuuٹs'mڴGٓ]>w7mڔ+VtRZlȑ#8p`δiuFrhٲ%7g?<<777ڶmvUTaȑlْÇ~z100`͚59sgggX~=UTy 0coo|i>Cر,X/,_C%^o9N0pkS϶5MҪ2V/ z4U?O{_"wRE)I7n1#֥!..ss[PL~GWW3x`4iRYWE7sog_قP!B!ēPuDFFu5֯__UB!kDF΅B! !B!$8B!BCHp.B!\!B !B!Bs!B!4B!Bh ΅B! !B!$8B!BCHp.B!}qqe]ךB:rB)877V}mK !+"\!&qFʴ!B!4B!Bh ΅B! !B!$8B!BCHp.B!\!B !B!Bs!B!4B!Bh ΅B! !B!$8B!BCHp.B!\!B QyVVƍѱо`<==筷b}>>>f߾}eomڴi7( >>>ϔ'88ϫʢcǎ,^;=zx}xb ˗iٲ%ǎ{!N]O ="n䍡eqBǬ>" mW*tā eZI΃Pyrr2^^^=zоh8p !!!XKk.Nhhh%K`llSY#!!ATΝ; XZZRZnFFFXZZRbgnmX-;'yJ|q2K 0)WMyԂ\ D͚5o ?y$]vw`cc+ŖpBLʕ˺/GWWWҒ;v`kk[*B tڡ\3Q^'Xuxr(\[[KpB `ܹaddTdy\v#FqWҸqcnݺޖ GQ֮]K.]O>>|Ȳ|}}ܹsmmr!h޼9cǎ%%%={0|pUOMJNNfԩ8::booϸqHKKw%00Yfakk˚5kܹ37n,PիWӮ];Zjw}GNN޽[.88kkk . GGGƎKRRRv_v{[[[<<.F:Γas@1bBi}X$(i-%N{Y۷o-ZR+ШQ#5kz[@@* WWW6loƢE aر5/>n޼ӧٽ{7DFF`ݙ={6&&&r)ܹ5rmz;wR~}NJhh(Æ +ׯ_ؘGqFv;Zנ ֯_αcx)WcG元!!!,[b g޽;v={Sl["""믹x"&Mbĉ<|P]lذA-)){2p@.]ć~ȧ~2e G.%cٲe|';w!Cp9r$!!!,]kײvҸx㥤h$VՌh?x4_mğR[~7\fʨV̉Oܬ`.Х,L 6ޑ۰-?x\zV.Y*~cmiPØ^/.~M˱~L+|?u ~OĨ߹AXL*~؂[r9:!y'cs&۲e͍U^vah hЬN%ޟ(l>v9k vE[-Xaab;dd2qEꙗNd˶GGfwE[6}Қtٯ1Uxhi*uK5lӓIok}6Gfȑjժ~""".몾2^^^l߾]=yf<==e 6 tttpuucǎlٲ幎ĉ100̌C ~TPcccڷoυ գ_~hkkccc\rddd(tС222m277ť򒓓ٷoSLzѭ[7LMM'|B-Ңk׮YpԀpWl۶-[ҠAloM׮]ɉ֭[? {{{7o}< 6ۻ!ċ4ǓHZ_{wVUͬYIDAT$<8`(X9iɟC!5):9'5+KBO9+)N $* *v˫XYk}ֳg-l ׳;u*Ҫ+/`gk-H4P K[׭AޕjeRߥrN@OW'׮~[UΎv4]cnpp#k#B󰷵]ܾO"_3ܴt+O.ܪ**9cgkCU9g~\~*)m^ (d+dfܶ&v6T)DDF_!``4ch2Q]5V։_X Snmv}XI^]5 Ҷm[FU`9svyg8q"xyysNN \WZ5O{s*UTT[233Yh6m"..S+V,ȿ/ӧOgŌ=ݻ{`n{eskNy ]Yh %%'cȪU;v,_įgλh~F͉VhAYg{&#qr=p5Ny?VΉ3qaf?/=޳[զR`kKVN"\⧃9}1ԛ9 1zw"O$%,#M"w{;[2r{.\I?}pξ3,3Mtx|#EƱp}P^=BBB+Tpv 5e*..PƏϸqpuunݺ۷/#F 4:t~իm)c." 4pwUF˾3Wr2^zηȽش[חiӦacSwjs!44+Z,%44;wfŋ9~8HlB޽ӓKVVׯ/p:""bccYp9:;;Jbb"@Ŗ-ZۛDmۖg_Μ䞞dff_|a.&&u֑&){ڴiõkXjFcǎf͚(_<:t`ԩ\|lk9RDPPeʔaʕ2jݺ5L0޽{G{_1֭[ٸq#544"K&rGw^1>߬ N4&s]W12 |2 bkeK}t<\KҬV9 F/ݶ]e_Ņk;}}HI&`bb]WaEodq ;&qyҼv9;}%UsxW+_iN:NA9Q]#RNvzU *9s/ 8܋b9/7oܼyظq#cǎ%!!,|||0ݝM6A&&&2tPKEӧ7/ׯ999 >K.QF ͛G&MQF >H޽ܹsvGeʔ)deeƐ!CyGxx8#Gd̘1,Y˷~kנA;v, ?#ִiSk^~eJ,ɓ3j(5kFݺuy7ͷشg͚5?GGGzIǎJ*̜9>?v1t|wOeڴiL4;RdI7oN```SOG@@+WfĈy }eʔ)y>8խ[3}tFIɒ% wW?kܸ191.[7}r_8˯*!O~Cխy |s+T-_z5 i.1d,K ?]ξtZܮ&^ 'vO"|*l>N"d*3gPV-KJJu?"88-~ }a숹¬O15*f%#R:S4ty"U4&"""[ulJUE/b7o6"""֪okt"*s.""""" """""VC\DDDDJ(X s+p.""""b%EDDDD¹P8 """""VB\DDDDJ(X 4NJhzj?G"0*R8wslzZII""sEĚ9sm5EDDDDJ(X s+p.""""b%EDDDD¹P8 """""VB\DDDDJ(X s+p.""""b%EDDDD¹P8zcƌaȑe_&ŋi(~zN.\`Æ uzڵk["7{Nnv/s1_c3nvKwb .]tܙ(~ϬY ɓt҅h9|0oҥK̝;H۬Y={λ8ADߊ-yۏk+t-eH^{޽{cggj=ZjY6wh-"'{[?Yk.GDF-7jԈ!C͡C{Zf͚ĉ4lؐsΙƍ1L,]:Cn_ תUh߾}eO?rOex1bׯ_'""-[ %%ѣGӲeK|||9r$tޝm۶Œ%KcxyyѶm[+s=?Ç;i$fϞ?Ç 55qѴiS0a#\s~)-[e˖̛7ϼ;m[9Qvm.]dܹsӧ۶m?@@@ ^3e.&|w _/gNgGa4f'2}}DzĔ>M{d)2 m'|{3vgNgGD k׮t֍VZzS4i>>>k׎?,KYI&Zʼl Y|9 ,`Μ9۷#F0lذ|Ξ=޽{Yn>}3fйsgLB剎fϞ=Svm"##ٶmϟgѢE@tu2zh0`鄅ѰaCϗ_~… ٶm=2fʔ)1c /СC!{ԨQIdd$.\`Μ9"""^:vb̚5Gwڶs(xv?֭[˳>66sa~mz-x뭷|2k׮ͷϕ,Vl¡MyzYX7l9x &L(/ٳY~yQ7|CNN+V $${{{-[ƀhРvvvҶm[V\yO2[899QbEgTy{{3x`J. mڴСCmyf2225jTV_|kbooܸqooojԨqך/_̦MxquuŅ_kך4jԈݻckkKͩ^:G-Զ`00zhJ(Q^{MbccCǎIMM\tHy*T@R +"ůϜ<7m'=fף]n_ŅVU)djX FҡNy>ը"=JczU£BIvL_@5U]РY%gp%5WҮ&(hS ++ i9tum)lϰs,J8ؑ8;WVD AaÆ1ѣG ZUxyyѿ/_;c~+v< 'N$22///vԩSڪUi'˱+UDrrmgffh"6mD\\)/x"׮]I&e&֭[ߟcRJ;֘ /gdffӶUdۮ;x -ի_bD :c4A[}ĔmAVջ~"+ddiR=7}܉#WRs>pջ9pkdI{2ؿp5pce[U6FTSS7KwB3g{g^cƍw ?YjG]v徱zxx@h}/s~cƌ!11 &P~}ÉmիSF "## \IKK7䣏>O>c͛\r^.ұn#443n8\]][.\rrE.[qڃNqZ&o<[jGXt)7n;;;ʗkR8<ׯ_'..ҥKӵkWrJTǏgwlFYv-CiL\\NNNQLV\m˯Zjԯ_?L @2)ܑWhV.ӤzWq!Z&N_-ԾFҵL &Vl'fmU$J64K {q*KP3+vēcjz6+vě٪+%X92s\!3W{16PIײ< -7oޜO>YfѴiS^}UBBBnmmm9u:uiӦL<'sYwwwprr߼_~ 0ÇӴiSf͚żyLQF >;FΝ;z([&88]G bԩdgg3rHV^ӧOo߾y5h """ҥ ɉ%K~iѢ˖-HLL AAA<端GOwwq\2]tۛѣGR۶{vvv̘1UVCxx8ƦpWXG@@< %K>Sbcci޼9Æ c(Tn9.>!^~U y"[`]b<߬ y hZ,S]l9ra^)ior<^,ge`~liQ`LO^Jǻ1[U5_Ya^Fȧ{*vbtWOlml ]{wOR^)T+9sF .VBTTܗ9""ZsE(9ܭEDDDDDNCs͛7Ӗ.CDDDDȹP8 """""VB\DDDDJ(X s+p.""""b%EDDDD¹P8 """""VB\DDDDJqREKPS<8zQ¹[eKJJy@+"̙3ni-"""""VB\DDDDJ(X s+p.""""b%EDDDD¹P8 """""VB\DDDDJ(X s+p.""""b%EDDDD¹(pEHH>>>z… lذtCfϩMsva^}p ߝ `"l>m=N&un]t7(p e߾}|̟?kikF@@}tts-6k֬y$_a }g""","Vmňlx ּ5GHY@UT(x׶%p-A>}wMǎڵ+vvv4hЀ@6oޜݘ1c?t_H6:kDK p1M쿇m;[is>nwm[յ^zi(pޣGMgYRRW^ͦM7o^w'NaÆ;wμ,''6n܈dbҥtuƯZVZE,ׯ~);"00-[?Έ#~: 8S(RRR=z4-[LJ#G @ٶm~!^^^,Y8m۶嫯Sς ӧ^^^ѫW/ &>>>߹͝;{)SмysT;kܹ=px Zl̙31L2n86m&L 3f999OM^8u7n0b|}} ࣏>`0}vz聏۷g…|Ǐv\t)ϹI&1{l~g>|87n[nxyyѮ];~~z<4 F&pUi?Oxe9r.pR: 1f}\Iͦݤ휻i(]'{X9D]zoNϞ=&Lrߟ-[2zhnܸa>8OOO4iªU6oތ` 00˗`̙þ}1bÆ tgϲw^֭[Gdd$Ofƌtܙ)SP|yٳgԮ]HmYu2zh0`鄅ѰaCϗ_~… ٶm)Sc<ȠA8q"QQQ)SSXڵkY&{eم:ֺupwwgǎ,\ŋ?0j(∌$22 .0gΜazz:,_ƍ3nܸɟ_ҥKlݺ-[bggDZcf8^iӦбcGRSS9}}ۿæϜ<7m'=fף]Qг;u*Ҫ+/u]WǃIҭ>M^xQEmmZ:9+b$!!Pڶm˨Qˏ=J>}S.իȑ#=z*<3L8Hعsy8>>Ui'˱+UDrrmgffh"6mD\\wkŋvM41/3Lnݺ899vSKw:TQu|;FRR /g}^&k׮oܸqn   >^zuYh %%LyTтMG*V#1&}[{pj&neg.uO6MsU\8ptW`K׳X3gt-l#-ꔷt׈R+̱Μ9áC0 l޼HBBB(]4]v'W\_=u5k֤u ֭[w>͛7d {{{J.MHH[neƍ bbbX`A777ʗ/ڵk1 :t(tRJqy_N\\qqq899D2eXr%d"cHeG.ͪDN]Lh1W{NM+>;>Ɓ|"ݚW)Mf W3iY*KKΰtwTlǎKBBv/// iӦL2 W^tX wwwprr߼_~ 0ÇӴiSf͚żyL帥QF >;FΝ;z([&88]2dh޼9AAAL:lFիaO?h """ҥ ɉ%K~iѢ˖-5PUaUB>35kOhڴ){Wʕ+ӥK=z4...w=n>}z*>>>Ktܙ%JܶO֖I&o͌3P76}___Jhhh J1cVLJpCq=HKK'???xg(Y$1yܚs]|C DUݚWgKw>8]bɯW=-ޖ5ئ-د=DNc}'"7ӭ{ř3gU}h%%]`&&&sfs~믿t)"R=WDQG"bM4r.""""R8 """""VB\DDDDJ(X s+p.""""b%EDDDD¹P8 """""VB\DDDDJqREKPS<8zQ¹[eKJJy@+"̙3ni-"""""VB\DDDDJ(X s+p.""""b%EDDDD¹P8 """""VB\DDDDJ(X s+p.""""b%EDDDD¹(pEHH>>>zcƌaȑe_&ŋi(~wKwCYYYmۖs>cZc?M]KI9Qm_XblyXO>v[cݤt䙩; ٝȐ,EYLhh(?g]߀h֬˗_Dl޼۷zBΥKh׬Ycu֖5kRrvLkԛs2uJBt"n%PҧV,Fwd{O'x]M>pߎ%[,ZݻwӱcGv튝 4 00͛7>99K2bRSSiٲ%ƍTR,Ynݺc>[IHHx 7{{{/^Lpp;5٦ZAmkE "mZflm֧1YԊ wŧFYv\t9"b =z`ڴiy%%%\`p<==y'pqqaє,YYhzt'Nаaϲ~駟S/ٲe AAA<1ׯIIIˋ-[ѣiٲ%>>>9ܯqwζmbɒ%1p@h۶-_}Uz,X@>}w%..^zMpp0mܹ=ooozAbb"o&M4]vl۶ܾ}6oO?-[ҲeK͛gnϺu?GEEQvmϟ| Ze˖1#EE= sNzM&MիW۷oGо}{.\h4cʔ)4oޜ#GCxx8O?4ՋSNY% b1/йcSvzrA`ϩ/>sv2b!EU> Mw{~x=Wh7iQ@?۟xo/fo g.w1géi(]'{,z'Y9z~3|MI40yM NIr2Ec0- F_m(*f^zw"ǻ c0sCsBտx2sv6o>HUz`8p۷ӳg|뒓ꫯn;eeҤI?yw^jժ믿dرc ϟҥKYtXk׮f͚ݻٳgNjj*˗/qƌ7"V&u.\I{J@nۗwjd݉W~Պ _zyC-6=Z;g?ő#V<^ wMU(ɲXz .\7󵋻N&Y7aD~?wݼ~ˑK/7Z2,1|} WҲX19샭Mf 8s)^~睑e$#aD0 F;ڈZQRN)';Vlt(.w)f{O0m &CFpD,遄ꫯ2tP5ko}xx8駟.pqƱk.<<<ׯãB o!''+V=˖-c4h;;;i۶-+WcFz-X"_~m{oooLҥqqqM6:t7oLFFFёjժ/? f͚)SVZQZ56l;vȑ#~i{{{ڷoٳ't҅˗/sBF5jDݱyT^G޵J,͛7d2OCQgȐ!TP'''^{5N8ٳgYbmڴcǎQ~}֩S>}`c˫L25 lmm Ҝy[Y^(ds>q4g/04䉪T,툃 mU$Bj`gh"`";DŽh|)G Wq!UUJ9hG8x#_@J<ݨ"6Tu-Ayu+EwelmZ/A4f3 Cդ%xa;'xnNL'NcPn@ʥhۨRF Sj4Z A2 E}q !!Pڶm˨Q5j~+W|@f8y$gaĉDFFΝ;ͣTZ5O{N8q_]R|4,33Ei&HOO7Oy)ŋvM41/3Lnݺ8999336FaYx1G{{ݯ~(TT }MSz<=<<̃/ڵko8~87n08yݸïǒ;"b*p;{;[r> ahYnͫj۬#w'D2W22Ҥz|.]bxJҵL2Su=ks˗(iI2ˑQhc"\⧃9}1ԛ9)pydN Sޝȱ\K65)p@޽c N:m6-[gjGFF[GG?spp 88UVqڵk[E$&*~Ν;m׏3D&L@ '**WN5tSNtԉDCz >Οϝ7^-| fbrW\],bU6{,41W6!jh׿l.6ngn8kljVrtFubBV9wZz6eJ=uY7 Ez;եGb/?#&UnG; 飻&o.fDڌX,)[)___Mf;5%..3g˗7nmڴVZ)Ν;Yf aaa}e?~@dd$[lwIBB{%++8:""bccYp9h:;;Jbb">>L>}נAK.8p''',YE,[ WWWKwm\rf͚ꫯ]D?ЩS'ڴi3| 76m___Jhh( իWᥗ^sΔ(QEV]'1fֆޕW{Ъ*[\f肃;f2wlx 6ϧ󡋜N]R x:|sfTRm_M/Tg$Lł-gyYwܫ-د=DNc}`LO^Jǻ1[UNo9uK~t}9tWmJGiHǻh],owo}6Z/P35+:~ZxFYz89أ}{/69so3b].NaEEELLL ~ Gcɒ%.wf7nc&;~9?ff8;YD)&EJc"""@|гK=LJhoK|K\D7oӧ-]܃[ã+=wJX#D"""""rg """""VB\DDDDJ(X s+p.""""b%EDDDD¹P8 """""VB\DDDDJ(X 4NJhzj?G"0*R8wslzZII""sEĚ9sm5EDDDDJ(X s+p.""""b%EDDDD¹P8 """""VB\DDDDJ(X s+p.""""b%EDDDD¹P8Ź(OΉ'([,祗^&++QFk.ٓg޽{ ^}U^yKU3f ׯ_gy_&%K0pBo?X]psYJ+RƬWqᥧkФznu^_zGcgkc)ח&B*F ?woVGi=]m[8OHH`sFŊyHIIaСo'OK.̜9u%Ν[pf<==pg~'sGȊ-(lOfGX<N.? O_㋗},]^Mkٽ{7;vk׮ѠAټy3F}RjU>qIjժe>[KHHx <-]'{[?Yk.GDF-уiӦYsmm?>| ɓԬY}8q r9󲜜ظq#&KҡC|||֭kZj۷ϳ_~|@@lBPP?8#FDDD0p@RRRe˖@7Ge˖0rHRSs޽;۶m?ˋ%Km۶|WyY`}ˋw}8zꅷ7;7ŋiӦ >>> UVlْ#Fl駛܆hdN"l>N_5cd-$l>v\1X2/G0'Hp## ߝ0ײdy]•∹u/Xay= B8ٳy݂S4i>>>k׎?,uփI&MXjy͛1 |r,X9sطo#F`ذa>|wY˺u눌̘1Ν;3eʗ/Ottxj׮Mdd$۶m,ZȝRn]FMtt4 ==06l/Yp!۶m3w1ev4h'N$**2e0u|u/Y pBhٲ%:jcرO?ń `0h"ϟϾ}puue?Ço[oƘ1cxС̛7}v-[Ʒ~ˎ;x(Ye"r'3r4*;ƳޤZ,ތU՛|+7h'/,}՗ 7XBv"Vd;ZǷV9ֽӊg̔b0mܥtF>S=5} Ynfb;x-K٫{N;ﶢsr&nfyka긕Q׋ջ|{ :f͚z/ٳY~yQ7|CNN+V $${{{-[ƀhРvvvҶm[V\yO2[899QbEo!.7tҸЦM:t7o&##QFHjxYvM`` 5k֤L2jՊjժѰaCر#Gɷ߯A鉽=C ԩS$$$Fݺu?~<3f̠t/憣#:us^{MbccCǎIMM%K͛DGGc2x'$u3g/MIez{`{g*.J)';hVo~pp#k#\mZi^v6{z3W5DJZ6N FT # lmp%BiWq|'^hč9$]dg2 n[;s[ w"/YayZ wqhh(m۶eԨQE߿?˗/wy==3L8Hعsy8>>Ui'˱+Ut,ZM6GzzyKA.^ȵkhҤyduwpp0ߡϙ>?6/3 $''SjUNHHM4ϯP:x -^2}t/^ѣ޽;^Ceܺ h?R)7ڃ#w'D2W22N.vdee[{[[lu;[>%`D%Zzy 3yZx-68rk1|#9thyjc'(p@޽3U^ݸqo3 `VZő#Gh׮nnn@nߋ :w]?f0a'<<۶^:5j 22Q2d?f02e /|yTF\\?qJݺuSNtԉDCz "" 4pwUFp2y:ԬW;9|zW+7ysY4#:fDڔuv^ 209׭~Q%Z/_-7"rw6V0eڴi񄇇s%F#{aҥ 6}sN֬YCXXyy߾}Yx1Ǐ`0ɖ-[ݻw}xzz޽{bM ''X.\hΤHll,{n-&111[ۜ_;qMA{>s7GAْ>cVfc*dZ9s9$%]ͭ'**`bbb<=RHk |jt)"KEV"""bi9 " 5oo (""VUNDȹP8 """""VB\DDDDJ(X s+p.""""b%EDDDD¹P8 """""VB\DDDDJqLIKPvK{X {B-R8YMDDDD䡒m5EDDDDJ(X s+p.""""b%EDDDD¹P8 """""VB\DDDDJ/d2MsHzziiirrEDD;{{J*s)lmg¹#h4JzZ*K/QNmKqWIIqsl2TTa>@zz:)))&\\\%+k0 ̜9|||ٳ'vtYiӦ 7&88Ç[G ˡC߿?YYYԯ_hƍL> Ҹqc"""6l?#իWtyf[laҤI|rA^|EjԨA۶m-]^> >;;;Kr['OF.N8رc;v,7t9ݻ7߲={OX< 6nW_}; <͛7SreKPh…X RV-VZEXX?#.s)s+B-]mm߾7|ooolmmyT۷otiyܼy)S  6$&&ҥh„ TTҥ\zKrWk<Jdd$111 4ҥqڷoOjհk׮s1KP(''p~m<==qtt_~4jԈEYG¹Zٰa/2&ք ,RRR(UKg%(( 6l@\\y5ٻw/}K'Oa#;{{{&MdR mΜ9׏[<駟8uYYY^GGG|||,]CN'NL@@@O>$;wt Mk"]K(ŋcooOv,]JRRRh޼9L8ZjY<~wf̘_mupԩS$&&ҪU+hԨF/f4h@֭)S ˖."##9y$ .t) ڵ:`cc#-[ҥ=:JJJ14777.\` "H۲e f>|8q[oRT):ud|2ÇgڴiԬYQNN:ܼy 0`~*VhéS3f̠^zڵW^yrl 4g¨PK>ԩSl޼UO?k&MXΝ^GԚI}iZ<~7^{5>C~iKsGmۖnݺ~zKcc1b .dʕ4i҄KhL ppptҼZݝ;9»KFppp =zi Ill,C t)b FI͚5qppK.lْ+VXҝ^Gnnndeeq<\x777KP8Goưa7n^/gYFF.رcDGG 4޽{s!gܼy%KZ<ݱŵd)Cs!44*GM&vvvGc7~yzzִ[nҥ>2Eo1tP}BBB,]m=L0'N?LDD}ti[b0vƍN:.^`fϞMBB-[ӻwoKϭ;X|x:vȬYqF~7I~駟nN#LLL YYY,_hhX)"EK/sN2220L4lؐ.]X]Q)>666&++ рјEDDno[[lms:::bggW,# "8[[[챵ѨP.""RD666ښF "L@"""vK\oA_X s+p.""""b%EDDDDDù.")BsGGGrr-}n"""""1)t8/[,/]R@),._Dٲe $==WhXܿ@rpvv.vE """""R|t+p.""""b%EDDDD¹P8 """""VB\DDDDJ(X s+xeţ8@%tEXtdate:create2018-04-12T17:59:47+02:00T%tEXtdate:modify2018-04-12T17:59:47+02:00ӺIENDB`django-tables2-2.1.1/docs/img/bootstrap.png0000644000175000017500000021325713542204315020111 0ustar josephjosephPNG  IHDRg=߀gAMA a cHRMz&u0`:pQ<bKGD oFFs(UtIME ;,-8ͷ vpAg4lqIDATxw\Ve=CDTĽ%_6QrSjZx\,Sý7( D܂z\:>\( B!BPwB!C !BT  !BT  !BT  !BT  !BT  !BT  !BT  !BT 'Y}df?|zLې&e!DR2i^âeFҲfe,Mf% ςHS^mD#J/d_|"Xuҥ_!;Gaրϼg81'k.lll ΛѺφzjT'}Ҡyl?>%7-q'9ve!&)~l@4My[!~REoz ?%{ک ?tS>\MRzsW]=sZ3fpwqR-_.ӳswp)$mIԴ6eH{2IcҿuuQ0.՜8٘ҦNg}\&%#c}3g/|ѯ=|-#}-czC=l(KU,*rY8Z8V}!cqo?)"]Xl ށ K#jY]]K_`NJ $G5{{Q&ZT*P= /] '15ֵ0Zq?oι[1U*z5 0ЪyoB*јAZf6--j#,qwWaШUr*j8Gӱ \hR2p2aBT;~շbҩaeŒMq4*[TՃU99 99O3~Z|ݾܾ(4Y^kgԼݹ6\k.0ELR:̙rCT/ 6)=vr& |$3q,D,ݳ]ې{ʂ~;({ ƧTՔ_ns-x np)$7v6)ǵUcA;Ys0VxR8QX0Fik$dIpT2F&܀u glߡy<%%r/ܧ =ڱܡ@OdoaT*6a+ЪUd(ŖSQ$fIךKӻE5Fx/GkS&iHKǢC"n "޼Y9S;vAD'ѠZ%kLS%Ŏ\COKc>CgBozf͌R2INvL [NУFj|Fs֎r%+[ad(g3dd6܂7X~Uјi~&!q,lJܝ> mwy5|nǣU`hKԱb&e6l '3yvy1k@3LB >ZMV?zZ.ɢ=16Ү<)ٌ[MjlЍiClR:+։JHxVߖ}l FJ(^<΍΍lgVd^!(*az&l:W1 Kes8;Y nȧ\"^cQH&%#tm%#r w͢?3Km~מ6ur#J?Km<\ݢ[w$ "%{ԣ1?HUER3 <^>,lŀ65x,.Б7]k/?ĥw.}Zڳ{R'>] ?] ,6~UQycϥpDwt.fdzol- 4=9`@ OBj'uf0gaHǖӣǤykMMǃj#ڍzvf|/ ")?_䵗ji\{5/. ;]jy&P ?k. a 8O\s6wOXc} k @lR{/eۭqy9SѤed15W3~7c{PJBQU{Kc ߄?nzܸR_F$G`nk=k:4J*AՐvXq rЦulLx[~9gE%ƒc_''L pB֨0}d9=͑ajT8X_ZͰ23)Wo7-جhCעǃ:l@4;fTVӆ}yFǬm]c;԰2uieBBj&ُ=~O㏋w> m-rqs>(-B߻~* =MQ]e#U6ݮuhUZUIWg5 P J¾rnS7}=kZԬ0Gbˠ%gZ E.U @0+r:f=߈"9-` L`Q{7H_CJ8;Yrv<ݛhmrL+Zݷ=MrkAZ·Ѫ%nLӐ7tl?{/Uo܀~ S3B󚕱0ҘoBB2o<ϪwZUgHroNUdZ;w.G$Tմ@\ ̍ Cz24`0Fqך6_n`pt/Z \JsKg|R3OCZi[.R8u !88aeR[}k^YxбA"QVjT רb̎ 8~XXA"co@Bys;&^Ҟmk2~"'-3JEq4*4װ``ۚxPpfklQnjD''0­5V/<֜=hԪ"l1=өaaJ_KQ1Ө)w˸8UL {mT!:1_4tH%PO͊/q!8#R8eԴ2)2Շnh }ZӮ5N>MF~|h152ν>~jhpDBQ#x/bv c{֣g3;R3rwG'J];B9|P`yMFZ6uޣz'EεqoMtB>bwS ,l_,wĴLT%Üj#r!(gɛɴIʠ[[̍߯q)${IzksC\YӾ56 AQ\ ckj^M/P_X"NLg׸G4FZC"^(f#NGXĞoֶ3$8*9;RΌf5 jWך|"TxCضђ_+듐8R3~;FFԌlV G [ ?`[-vc܍Oe ~=B £$͍x=_qkwsv<ﻡ_1ߺ:QI>tTvyqf/=*=Ȅ4ݹwFz4VO]bSonMKR78*X{.y?kbn8S2 JfƯ O߰9|;)ڪpo#Jh*Ӈdފaџx3+/\Р*fz|Q|gCK XG]kTϺ#Ŧ˟[K:wsdǹx] <.\CVѠ9gDhL G5 !DQYs ]_&vf,lL>OC Qh[NJ#_5 ,̂\vc-}ZVthSE^'5#قؤ nHBj&Y2YفenQ !5fULӣ__xm)ʞs'6j[-gj jhoC[7Tl4ƺO#~9c< ͙طyZ&$eׇQWd߸nmӐGY5jZ0Hbf0,9aXDz123`[,ˏ5`[NUMQIֽ%@VaWوq3# {tW3燷_N%wo^2ywc+Xy V2bZ4{FnRS3_oʼ߯CX:O7j[׊9t6vk eafxޢvhjs@7o?;}qؘѷS=rzWҵI8Zx \gյ*/ssêY%AY2=>d+ mX14i !R^\+K3е y(^fk'gՠg9 {^ >'~_BLs !Hfvy0Ƨrf΄D!(#!Ŀ4c拕!;:"uB!DE#͚B!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!gB!,[z5+V_~|g0h LMM9|pyo( oߦFϜVnݸ>6m~Zgر>}3f?33m۶w^Ɔ3|plll^h~MG}[oU`^JJ IIITZU7m Nr7\$(*+OgN_o,9Ƶrݧy?NPb2emJv+w'+ pUdR>U֜Ғw}]p5jTY)/ТE fҤIl߾}}'wwwׯO˖- L_f /.jhoo( لF'-2kkuX+?.q%4FO^+5V.QlHLRub`s,Pt"%|hU2a|?_k,--y1bDy﫨ؽ{7}.0fXXXp]=թS'FA&Mʵ|4*fZݟ6 i] K9SDԌld52's>\,TV6u9VBǓfMQ|||ԔmonnNyfygB1кv#;;zIiYշƽy5*Ŭog@x\jygUQ=Uٳgٸq#ׯ_'##M2jԨ"}܀tvšC 11###޽;,Ќ/_3j(Ϗ?ljښΝ;3rH,,, #))͛7o>޽:ubԨQ̞=Ǐb \\\ w1n݊/XZZҺuk Fڵ޽-[زe :ubѢEGaV֖v1l0L_Q~mFXX*U}ۗ:[SN M4aРAZ>'';vo>BCCIHHJ*jՊCаaC{Ӌ~<NbܸqL׮].rŋٴiFw)4 ˋSOpuVZŋ̤zxxxoOJ_#MEL>kWaX'jۘN\r?{ǥUpj-yuuܭƧd?tm9–!tjXE܀e@NGBVBFg#Vf|"t1|_nLGchW݊e@r5+[ΨUîs9t-DӲ0heB l[}Lbзi嘛Ʋ79|-L*޼9bRL>'bG%PQi|{f޵m۶,Zk_q=ӧ#GDri֬Yj-[T(VZ7߰l2>Lbb"vvv3x`LLLJ}>Z<ʧ4J^ZZ ",, WWW鄄0tPҘ>}:ͻy& .\ **l,--iٲ%4hР@Zy֮]Kvv6Wƍ3f5jDrr2+Wˋlmm߿?oVRހg?c޽K*Uh۶-Dl޼///P899ѷo_<<mLŸw.3_oJ u%gQemBZf>a ,=mDHt2Գ3;r܊L@zcPJIΆjnM%C6;0LV܀V^'uӳrPrQkJ27$".U31|7…G%gD&Ⱦum͸ /FuQz:~01Ѻ9ߞ ᅬΪUq̝;Z%?~W믿RJ嗜;C@@5nݺ\t+VpA{*W\>?ɵyOq$=CCCf͚Ż˩Sطoz ;;3gFǎ fwfΜ9($''޽{~UVw~g ;QLL gΜˬZYfq-',,dF](ٳg@6mqv|Θ1c}6*ի/>>>8pŋ(OWqvvV\\\[*999(Jtt2j(YqvvV̙[_qvvV:uT ŋ+J۷ogee)?.WmٲE7u󼼼6m(ʎ; lkҤI2d%,,L7Jui;wN7oƍҾ}{e޽}MKKM6a[Y L?z.N*0ܹsJgggeݺuuUO?($&&*'NT>}(iiiuƌ8;;+;wMsұcGY7owK.__vMqvvVu覧)gV7|eW_)J^ggge֭/\PW&})0ݻһwoݴS*ʆ  ,;|pY9tPy瘳2f%<<\Q?Ϊ)CP3tIJx\rf2b)ӿcA)gS^OVS?Ki3c['9=KAӿq)Q ߰xÊ)P`[M?=@ӿ?VS2tӣҔN䮳:88r7 +Ο2gS7.+oSEQ﹮8v4+;GD_)Wo?_D3)'nDE'8K:fQ>p^W}R.>QŝoO̻;;;+;vT%::ZQET8;;+J߾}/*(_8;;+:t(Vҝ#ʜ9sggg_~Jzznk:YYYʐ!Cgggw]EQn߾;?Ν[coDm,6l` 0*yJ*|E@n5nFa&"_hQnݺ˗uӃ8x ,Z@ul.]0aBXv-'NW^}500`tޝ,VZSNRx7 rqqo߾-ݻ認MMM;w.DDDoذd\]]2e FFc^z%&Nʕ+uh޼yf߼r{饗hڴ)%ݻٶm{ mzܹsR^="""+IڲpByԨQT[C, Ǘy#[tndWZR=\'+[a[LӐ^t jOd(:[oD'`iC[Wb" O(16)̀VߚԀorm]+ܛj O NˇFbp&ਢYvvb!-ѨU|OObpYg;4VOo̘1tEw]2d(B-Oti֩SW^y(رci׮Rhػw/w}<뵮˧=z45k$&&e˖ի (z{{EF1bDի3rǖAժU٬Z*rkK̙pj=z4*)gs?5#} }[9 !oD—:6T'79wļ9^ŝy4oPu츜MtbX&^ ')-K ?$:ԩ)z˺A"Q1֫_ͼа9ux_.Dħ1m%-jVƵ5UѪx:&S2 vt a)E%qN 59JL0/m3c“~#} C;ba[d0zQOaONێnpuW6ޥ ZWS5mOOO6nܨ{ Lq4Z ~~~Fhh(ܺuePTCWq}򎫢b4lذu~K:yǏ/QZyiFߤYڨ⾈M$;v]K__f͚^)'yi^MEqy;"7HOO===233( i&֯_O\\nJnݺX[[sRףҊg;伦~,Z_~[n?ׯ)S;Rjٲe[3a„GٳgiԨ3/85g/_&99;Ƀ_Cr 48X1M l-*[CebP[OBO"3[h%g^޼|`ũlOc÷.щxwn\˒>>?Z͚5k裏ѣuU,x U}Ǽ𔵬,])S5k*M4QQZ7n<9mfff/kyYBCCuMIII:WV ͛ݻqpp ++`ݻwh3g3QiӦs% Es+Yvϟĉ?78un@x/I=*cV""Z:B|1skW)I2,5# T3XvkZŝT.ƕܾX*X^[>rOԱ1} C!)dSN7&M%*=κ.[[œzR65u=y/_Npp0NNN̜9m]xS6l@Qzy7qqqˠ,}Ӣ}Xׯ/m*upRر#;v(4?==}*0 /*@LyϦzZy_O:UdUqXXXʟYdy/)wvv.s{i j(OrrnCqeX!7xڻw/pdcqoܸm9rѣG3h Է~ˈ#tMYXX7yfvڅZfTWWW222tpݻ6lX|;V"q5] s5vXﱯu{Is-K]4?rم+-3Mh*@]1{DħVA:v=wyIKK.\~\zFkͳ\^T{=r?K,yB_+W O^ZZfBQx CM!O=.ٳ@Ajj*|.@}QLvb˖-kxBBӦM#""&MxիիW'--)S*e ĉIKKѱȧ6=jԨάYټy3v\S7nܘ:p1M_{.ФI|||'~)ꫯDJJ 8::CrrrIC%((~)SPjU %)) ===>2y1vz?׮]G4i҄u;dnʱcptt$!!???rrrpqqۻ2ܹ3c͚5TR@222e޼y%vurrbΜ9|g޽SV-t_-[xV\+6mČ3o!..NϡC.T[n Y~=ׯ/6?oSL)URĤռ7&5uWn]Μ9֭[ٺu+Çgܸq||Y mHн$~;Ɣ/R+3CBIJBOS&4^IEp7=ulH<5a YރwF^s_ԍ0щ7z;Ob_3#=ƥr?5Zg5)ɿ9gbz:Cޱz=~xcJth`1(maefݸT1ЪiR>qp(}JzZsV&i܊LDQrkGu)iXيkqV } YDA؆ J=wyקRJ55kǭ[P77bG>EӤwqmVʘ1ctywXx12rHΜ9/}AdeeGjj*-ZLJtJօgQvm.\Ⱥu밶&00tto(Xhcǎח뚷«T’%Kv DYv-]vrۥԩŋu* &P^=ܹ{_u8t37!UR 6鉝dddoW,J>c…?xxxO?xB<==Yx1-[$-- oooݻGǎXd zzz9yΜ9=}}}N8ݻwgxxx`iiI@@4lؐ8~xf͚3ܼy4ڵk… #{\YнNe˖h4;՞YYY=#`y155а\Fi*,hQ2c{cKRg5ag+\ZDzx8ӘҪW●:V K"695,ԣ1+mSɭ5uZ]Z5߿ݚQRΌ "1Чe5~ӎ. >z䝮qonC=5i/VLx̸w` Y9 [Wq#od&S}\pq !5T3gGcyP<΍A5Z)7NNɒٜ[>ǝoO|Yv-$$$F >}:-*ãZ"I;< SN-40kСԭ[fϞ(4k֌M6ѥKLLLpnݢA̘1+W*<܏ѣ100֭[;aÆR~899yfyj׮͝;w Ύ?:RNPq8tPyo!Ю];233ٳgOB{񹩁ßw/]MMM9|pygGoڴ鉟]*sKf]^/6=w}eƌB3!B $nnݺDGGw^mVOבWU!BhO4 I,\K7aϋFbٲe̛7˗cccCBBsivt/}ׯ_/ӤIF!BxYbb"YYYXXX/3xr#FвeK6mč7 gggK>}iHPP[ly+B! xB!DEB!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!* ΄B!*mY%MVٹ4c/9e 8Zn:tjX~ltKS}OZ4\勒ft+BV4VxsfS3lL i_ %)-WZ],BYp8MPM/ cΒ9sW>vccc Rf KOFHduTLRCgB<4=Z5Y9 ?zH NUQ;v+f/+WfŊ]歷*m.}t:7afh8wI.L(ou}eD' FZQ;;qD&R֬ˠL}1<\XtT}31Y{7 5#>T2Mךx9~bpY9Ԩb̘1)(`bixkT*XsIiYiQɯ65l>OǃMJOUʻxիGΝ9|p 6lXm/.97jT|)&:6ʄ`aO%=Am3Fu[}kNތ޸7c|Ç·phK:7a䏧!-YM"ѴKS,9x\kz7~<ӡ߭PO1cX2Ythf/|@FdQOVb?纽gBIJbH{G%/?bS|EѬ[ħ\"+_߸&e0#q}ՇnkMmz.hj^;|}x{bT\̶un"M Z^0}0Yw4-H9۝//F][3ފa?r- c=z4EOe,hR݂-pZnЪUtilSևDri{UԂ K~۳4f^.`y@ךLywa4DjF61IM ܂zԥ]np{}Ftt⍶5Ƚد?w0_ckLfʻ؅jUֵfI&W#67`Ynfhx۝kkM~'F/7 _ jݢkGetmlVQ{(pn"hSJW'Ŀ]tJ}i1$MRvUS+@zxe+\ #<.5w~vn}Kc]y]kU5PO|vBptn ChԜ QPfYךÛܤ"nr34׋Pճ]z}s@)=9CY9iu M g͡k\ z4-g{'#W2KxDOS HS?D\,Ц{/sn"* A>O!4j~|y0Z!JTn]t¡C77e>M^$Ӳ , jU^7]E5Gk[TеH_w` zJy^x/Ct#zٙ9Sp7>ؤ綽wq7VY1%u{ ժUԨbBFVj-Kӡ}B<^g Ҳ̷amnK0gUs[$ Jh<~67~HL˭e.Y9 *Q0)Vhb" ԏZۙVߚ7(֦\ C1=8&tؤ "fqn3; ImUZ5Dž;hj\4/, "DS3iVÂ%Ü S jW ݊ah{G,۾&YG5͗DPqOmk0W}jxF,lH ({KYV&vhVhP͜9o4cp nɰШU\ߎh{7{؆CZwvP_! ;y3gC9q3 ^QY‘L! Rh̍Αu#B_Zj]FYz5Z~大ӽ{ Ô)SKg~XXXnh] ΚC~%Ü|[5MKC{s_hsۦQg̘1p_}UV-4?www>8ѣG4hPyQ(s]EG.]bN6SEY_f/IX_S۪emB-k/ Veڬyy:tի ;uǏg7CR.)`ڴi̘1)Sɓҥ ݺucJ'22u6nȈ#@v|Wߟ_~~ oooONDD...DFFÌ3ڵ+m۶eРA={;vl{X.]ХK֭[DZccǎ̜9cm6^~ٳ'٤dz1EM׮]Ņtqqqa 8www̙CZZ.Wٳg۬oҶm[v_|Afff k.뇛o6>>>Ef́/̨Q +PF %{|.=;|~!߰ZsY~~FwSQ/sYΛݺBqW>ӡO !2  رc100(4>W^)rڵkcgg|Ypqq))W^?~Lrrr8t={$== VZży8q/~m}vVZO?ğIV={68qfΜIll,+Wd˖-8991w\F{f֭ڶ/۷I&l2֭[իYnÅ޽;111yyyѽ{w4 sѣ|],ƏAXf v"<< 肺m۶Ε+W3g#F`Ν̞=o߾"3︍;͛7ӹsg>]V]v1k,C&M裏" %K駟}v:t)SHMM%%%qѠAl?;B}p)ܞƌFbԨQL4Ij:u*{Ύ-ZZ.T6l`Ȑ!aÆqYvɨQJ׀S. ϟ'-- FժUwߥUVh4dذahZpwwСCO\.eN]`a{m(pe k5пe׹۬z-*u'콜|iLט-r YѢfe#M:I}kMR3tsdX'F!ܛ;qk4PˌM[m;XA8Zctoj[GP*TpѣiԨ&M*OΝ9tnnnxyyѩS'  Q4k֌"GZjҪU6m*rAq!nJHH*'' #܋q^V-4#O^Xz5}ǏRJ4oޜ/ФIݲ*UF=UpV^哕ŝ;w uoذ!fff[@nݺ͛7]vq6nܨ{{{A 0ӡC<<<קJ* 8]vqMRuHLbm]uƴzR`Yp),:zX_z*`0gL ,j ~Nb~whd_I4]]ǦejۗtXz=±0dxG'FcvL܁ͩjn /]yl=B0E),*Dt=Ǝ+f*28(ZnJbӦMdeeѶmnAZ "))IWuUj5jjV?ZNBBGW#?P ٳlݺZj߹yABKgajjJv8p'OQ* Zn }6+1ׯ_}122m۶Ν;3o޼囷GϚ5kBuӾ5jl8ׯ_5FGGAݺup׮]ggg>CǙ3gPEQXz.<~xY9<<4*ҲtCSJLXuЧ!6V4V<΄ҹ M KOkW=·QDMlĖh'^_|djU5jh|ގEBԜ-Zzə3g8y$'Oh4t֍5kеkW] _a֬YqE,X@^ 5YXX`ggǚ5kgΝ:gdd0w\BBBػw/v=VȈx=;}4?K.υ Jg?L*Uhذ!׏s%9s&ָJqرWrI֮][hŋ˗Yt)&Cq=͹uaaaDGG7k_iii :ݻw}v޽ƍ_u}Jm6|l><==|Fyswwg۶mS7Mղd,X'Ӈ>*3g2| 3G?-SreW'L2;вeK:u*6mW￧z3/???ڶm;{exyyg,7ĉYx1qŊE5jgޣFE>gϞ|dffꫯ2n8ׯ˖-C0j(f͚ŠA066M6xzzrʕ"˳[nƲ~z.\H͚5?>5*Ӈ;wK/xbj5-Z`ҤI\lmm6m͚5q\t &( 4`ĉ|7$''cb"whXvf,lECܚQ2{U[uV&݀J }2p1vzD^Yۯ2hiS O7G}:Z-Z ((ХK:ʴipppͯW^yQsAV^G_|9t4lذKocaaQY#::ѺvE!*15*K9ji/1kX!(,,, ooovڅjb޽۷O2p@ڴiSh;wA^055-rEy?P =ztygC8٘ҦNg}\&%#c}M뿸,8V[n-P땓CNNDEEQf" J*:06ms}W3|"""o8wj^zGa``P HNjظq#`ݺuxyyf5kٳgIOOgȐ!xzzpqq?DղxbN|8kfѢEҥK>s43`ϟOBB cddĄ h۶-.\`ѢEaiiɠArȋۧGӽ{w ~bccر##G/͛4hЀ/UŊ+ػw/QQQXYY1tP Ksdddп.\!Cիw:t@8q"-[$11.]m6B&ZT*P= /] '15ֵ0Zު3D'3{U.kMv6)ǵ;A?g˽OiL;F߰|>( kVך`gaD|J&s^kšCOΠy|ZSs{.h !kȆQ=MSf}j5j ==={p_6l{xxxi&]NPP|tOOON>]S@^8~8@ny!zIzz:>Zyq /^T`Z~?VZ1{llmm9q666̜9XV\ɖ-[prr#GÃ޽{uRmחdǤIXl֭cլ[ÇsRo>ݻ裏0aaaa0ydz?~<˖-… ݧܹ7yf9˜1cOسg[ F/ voŋs玮7mħ~իqi&&OW_~emBNFV)$gs;&-Bfsw^Hx9kG0~ys%jT1fGcy4& "%{ԣ1?HUER3qkw` H$9=ѴoMJF6yӠ9[>twmVۆ:Κ÷ϜWщ:2{@3_˾+]B0ef߾}qqq @AZj,]7|۷yoiڴ)Ǐ)AvwgΜrk~pssرc2w\ԩC֭2e ;v ))鉷e``qPocj5ZV +SNN:8::2dIKKCբVh4jjѣG8x`prraÆ`$͛7ӢE ~wRRR;;;wuMۧ1 jԨAڵҥ 5ܜ;#}-[ޞaÆ;Gnܹ3uaΜ92ӷo_4i1$((=(;}8o:JH#:^wc2{.5ɒf}9a1ɜFOBRըѪU܉KEFTlĻ]`h+4juKa_وq Ƶx92g},hR-xupd:T2̀[s;=ӻE5,5+Q!t@@;wr}>s>cVZ;m۶ ƍ֭[0`SNe͚5ܹɓ'w9;w̡CpssˋN:a``@PPe5kUV ZjiӦ"4hb֭jy򚔟FFFjժZҍ[nϵj"((SSS psso߾XZZ>>ϣ>666zzz.]p%~qf[n`ee]mohh@fffMBNU vY0P_Cfvw9fFZ $^"HmE\jLMMgܸq\xhZ.0Dtt4ZjW9rL+? ٳ|;={ < (/_mׯCݺu`…8::2rH֭[GϞ=*>=͛7G1uT<<<077'%%EQP8::w||<ϭ(K CNB +cT*tMS3 p86ƹ%^n 1p& YnX.ұ2-+s;c~hW/7:vڸշ&:!mgBȆyZ0]MZԬLx\ /pй̂FhCHHgϞeѢE+ҡC.]ĺu cΝl۶aÆСClÇ a…owh֭k֬k׮cccìY ŋ,X^zΎ5kΝ; u`ܹw^vŠA022">>GjN>Mbb"ϟgҥf<###pB]|}}Yf aaa|ܹsW_}sss[޽>>>4l33YsE bƌ2&Ս7rQnݺŬY*C!ʒD"'oFsf4E2mEb2s#=9;0k\ #^3^D&V}7s)i~tT3m=+NGW1c-tY%c}R3GBj&o*]7 s#=nE$Btb:#(*!?Qg,[Xz-̙Cu7n̂ طodÆ L>N:0tPF… 4h>>>,_kWNjj*={Mj,Y <==c:uꄧ'666%ӳ9s&n_棏>e˖nݚ7nЯ_? ٳyWP!ċX2 i,lEC܀jbe3|)4M658YI4b-:·{o0_c=x@ ftmeZh[ף-g\ľMэ|QbcaĠo3d 3tsFxB B'Rb߿LϟO||<_}Uyg剜;wGGGݍAzz:;wfʕ4mڴ'j{%$$)Sφ ZTB!^2ZSqaee{g'|w2BS! !͚B!Ԝ !BT  !BT  !BT  !BT  !BT  !BT  !BT  !BT  !BT ~M```yU!ɩT뛄B!*iB!@$8B!@$8B!@$8B!@$8B!@$8B!@$8B!@$8B!@$8B!@$8B!@$8B!@48gڴitwww-[Fvvn79r$nnnݻٳg}-صkWgBB#FՕ+Wm o>\u‹OL\rsk p.!h2s璒_bbb1c*U⭷"--ӫW/fϞ7f&Mw9͍>}tR22rko͌3ܹ3;w`׮]777~m|||=ϒTYYY̛7ݻرc -g|MڶmK׮]/`̛7ɓ'+©SXf ݻw{lٲEw}v...sr۶m+2x`N::21ТRc?bcd(gԚ33<9~MyC 4k !2s駟rq:t@^f͚{|׸2bN۶m˻ JW^?~\cáCٳ'dddj*͛lj'XxSm۷j*~'OZjٳĉ0sLbccYr%[lɉs0rH<<<ݻ7[n-ն}}}INNf߾}L4e˖n:V^ͺu8|0.ތ3C[vv6ƍ#--|lݺUhM4 Zꫯ8s ~n{>>>`ڴi+cDzyf:w|@dddy+)ߏ}v.^ȲeشiF+W0gFΝ;={6g߾}ٳ'm۶L0&Nŋ.1,YO?۷ӡCLBj!%#lnǤT=alP{4abdd0n7 C7~| I<WZp;gtZ4qT޻*ĿF9 UV9hKV^ͨQ$44OOOݹx"K.iӦ]v9s777.\@ZZnnn;vX6mڄ)SLa;e``qPocV-Jt} ˆ#HKKZFAOOTVՌ=}}}tʊ Th=VFAV*o|r,--uegŊ0uT{=># 78?~<̞=!CУG ٳgٹs'F*ߧۥKW;w`dd=fff̘1:u*{Ύ-Z[qƺ:ubΝ=CCCݙ9s&oʪ|h4V.ZB)f0ܝk5~5wjշ &)O7GupBQ`i{jͽԭ:@pT2ƶGV"7BPfٍ7طoO>>j ֭Kpp0WgtܙC憗:u u@f!44UZiժUM6A8t[n%$$___R E133@UZU7_KQjU]m۶9!++;wwq6nܨY`[q󊫥3`ѣZSN+4l333֭[G`` n͛Ԯ][~㠧Vr@nFScӦM4h_>:t}}'>_DzKm S}wW1^KcULض&osn"AQIǩitO܌b[ULE*ڵkXYY!_>DGG닓Sȑ#]Oݝ3f0uT<PT,:?$=PףQ B0n8n߾M^۷/dE4.JRR޴Zmfggckk ͳƍ3N:hU7Q;v?~+V￳~z.\~HΝiѢk׮}y?X .pïڵkYkQ~jXТfegּ0/at NLNbga[}k:6_x=ʌ_.3Wi !^2 ά 11333jbeewu/2x"[FRi&t~j"(($]իWQ888=˫JJJM{v-<<]Cz?ȁ={[RV-oal5jI||<Oү_?ݻ>666@ 5kV۫Y&!!!T^]7oQFYxbM##s޽{ٵk rk9z$&&ry.] `dd?.\(նˊcֲeKԩܹs ۛ 6ЦM^z%j׮ӹy&~~~̙3B5fyݻپ};weƍE>oYy%ott4 ,ŋDDD_ann=ܺu0o *u7n̎;z*'O,P VYl{!22G=-rG$rf4'oFsZ$6_$&)nMl\> \#!5addˇL^n%e Qf=;B0yXlNbРAYSҹs.'Njj*={Mj,Y <==͒%K044dԨQ0h B\\ܸqԨQXYY{tM-Z`ҤI\~2m45k\l<ćp=IDAT73~LdB<[о辖=j+{&l<ϫ_aϥ;Lۈӳ]"8ܿ]n R^JExyy`_YB! &/>B!@$8B!@YS!3!B D3!B D3!B D3!B D3!B D3!B D3!B D3!B D3!B D[;B!XNNNZN^$BQHB!D"B!D"B!D"B!D"B!D"B!D"B!D"B!D"B!D"B!D"B!DRݻw8q";wݝ4+W0|p>|8.]{qqq)7mڴ.2g[&ieeek׮R/ (eV7ndĈZ/`֬Y彋D|J& N߯~~.=ӡ[ҖUBL0[[[֬YCBBg&;;Sѣy嗙={6ތ7M6QF fΜIjj.pгg. ^R-|r޽;XXXnThL*:1+NanǨudxN\*{/, !Bg7oߟ+VPR%ƌ×_~ԩS߰aԩjf˖-LTV m;p֭ˋ5k?{wU?p0l l+**k3s#MGsrRyR) ) \C1W$qI\QDA~#cA}^ҙ{|0s/mۖh >|8n ҨO26oތ+u֭[Zܼy u_W_}ssꎏ9Bqq1DFFNaa!G׳h"=ʲe~ױضmׯgӦMܹ:0w\8z(̞=,֭[͛qrr$o6 8J6mА &Tih۶m>}+WR!q}lHH#88_~*I}2e 7pBN8A@@f{ll,,Y.]ݻ?>aaa,[Ƭ'bmm7|Ð!CزefիW3f ;wfӦM=+VT*ě3prrzb_^Ͷmٳ'ӦMZ2 Q\RF; t:ke)';]-dZrv?_Lc|Ղ]R.!kβ]ьoǻѧ%c֝C3û5OP.e]X G/l%0&045fŞKDKԷdZ4`~ħ?^$nau!0՚1aOLL ۶mǏ~j~Oڵk w'8uqaX`7SNL60rssX?B-aaa( J @nq8880|pRRR(((@TP(EOOcy{{关3f<akk=3gdر gtڕ 2{lJKK'1&&8͛3m۶ߟ[j%~~~ti߾=1}}}Ǐs]Iۺu+Zb̘1888믿ƍ+UNNNзo_Ξ=ľ&''5666,Y]]]Kn~{HJus`{33]<]lO\Zz4_ gKcmLռׇT Ȁ6VկňkdF/74u tݚsbYX0L\i/",:jFߠUC3 hym:z7{6Ɣ!l1P*xJST)ܦ5b BCCY|9=nݚ>KpB᧟~w׮]tQ3VӧO"##qss#""޽{c``JcccMmRZZןX666Zu9::RiY"## %11T9iӦʊ잸СC`tЁ޽{3dȐJ&$$ЬكVVVԯ_xRU*Uj5`jjַo_Μ9իIHHV)f*Fad`-OVPT>:٦MJO?]ty :f͚ѳgO5SNsz'H3}Y j7r3Uz.X\:*|蹞buφ*=_&JZc _Em |ݴmdFi\ϼGBz.kӨ.E=ܴ6^7gKq _59+++cѢE޽:t蠵LJW^ytlll/*sQFQqy,OOOfΜ?`֬Y顣S!P1Iz_Zپj׻s <<n,բRr2B}㖔1ֽ[4zHCS*yn_2JJ|*u+F.guZsڵݻիWWH̎;Ɯ9s>YJZZk5MN!$$ZM׮]-J5y9 E>:ݻw5ccciڴ)M@@ƍ]We_{عܼyGGǧ'::3g|rk5]o&==]\Tj'55U3%>ۓ>vvvّêU;2qDԔ*ٙD^rEΝܹs&GRxj_O:EHH;vdҤIaddJ}[Y鈊bd$ަaZM 6ו8̂`oQČ{կyJ;ZJ%s׳Q@pl`̹ZQ1%gqqq|xyyq];yШQ#Gxx8III|$''/kաT*qtt<.#00^xA3bңG,--3g*ӧOd<<<*S kkk#<}Tvލ)1b6oLDD,X@ktUVDEEqAbcc7of}ܹ3̘1+Wp%͛Z0bv)O&;;J̙35qZ̺u놅۷;vh6X֭[GRRwf˖-|SP(Xr%v֭[:tL7o@j5νwy(vI!Z&k~3#浩SKŜMb~I$U_7Ķ''ȼV{4!s*nd<`VKa]Mf݁$eL [_n2+[rv&LzҰaC>ڵkYF˞k WT|reԩG0fϞM||<#F`=Vn]eժUL6^zо}{'??ӧYt)cǎȈK.iڛԩS+ϠAݝ4-ZTv9޽{/LTT˗/4!իǏgҥ1VZiӓ?>kּYZZ> ?z-ƎK͟xٳgs5 ĉi߾=:uOܹsÇqFx |r<СCYn'ORxj_]\\2e ֭㥗^" ӧW{23]ileŠ=^-^s@+^`ˤ^gvIfWKWXʒɃ[P<|~Y<=-mTZ^a(S9M rZˆXpU&nkgi+:e2q_DDDdWMC=|3!B?|.BQHr&BQȴB!D "#gB!5$gB!5$gB!5$gB!5$gB!5$gB!5$gB!5$gB!5$gB!5㫻B!YNNNU*'I!iM!BD3!BD3!BD3!BD3!BD3!BD3!BD3!BD3!BD3!BD3!B&g|GՋ^zQkҥKquuz|;w777LBVVVuٵk^^^ϥ.Z۫\>;;rrrpuu%!!aÆ=S]ҥK>}zX<ϸ=|n:cw~rp u{H_aoG<+1cJ7ǬYhذ!...T*J߾}5p),X9sh֬K.e֬Y\cT8p 6]k֬[oٟ]/9YD'SH`Un;-á,sKΒ8y$۷oaÆ8::gݫI6l]txx8xxx0n8|}}}6u֭8(eee|f77?%zNҘ.k+rj>c9Z©,sKlll aÆJKK)--޽{coo_SLPk߇ O. ,мpBYx1|/( <<<8q"Zܺu?`cc@pp0'((i۶-2|p|}}9y$3fՕ;wT*Ylǎ#//&MDΝ/ ̙3ӷo_n݊P>˵kh׮sT,] pqq_=pҥKd…OqUcӻk׮eϞ=cnnΛoHKKc={===G}!/ݻܽ{4i]v+WsNJJJԩSNܜ <==#00޽{Fpp0| /0eJ9/ʎ?gΜ5Dm%::Wf@+IܸrL;ξ.k3# B>>>lڴ CCC&LPm۶qiV\IHHHY=k ٽ{7',,_e˖ '&&&|7X'N޽ <;v0qD&MDRRP>xq>SgĉZ,DDDtR {.;v`ٲe޽>U9Gy⟩H]J^Q K hcCfQיݚɃ[P.e|Iۘy_ӅB B"?O90NX TwWCb֬YT>~ +V_gժUl۶Mk߭[ٳgyW;>Zw'urqaX`7SNL60rssX?B-aaa( J @nq8880|pRRR(((@TP(EOOcy{{关3f<akk=3gdر;C>cڷo-#F@___^KNN +++ZnͲepww߮];<==ѣ...yfNJqrrbܸqSNi9r$;vu֚022utؑs=U9GgAx,Dy?56ziO=tݚ_TܛѰbC|ZϘ.&JL l9HB=>qAW{'sf͚Ň~k׮ЪU+  <NJ@@@kԪ>}!22777"""ݻ7T*066֔o۶-\~eccU#!!!!22P9Ge睾tmRf>Mc{`W Ҕoϰl?y+7sPr>N =z%XvgWw7GCFΌe>} J&1ɉ ׌022bȐ!tС^ӓb8AOO,O >h9J*--eX;w3њ#8OkgF c4hЀkVWl%6l`ܽ{ݻ|rjz%ԧO~GƍG~~>3f`ŏ=~aa!\oV}ZѬOkê⟩ym\b_-XfLq*YS)ΌB^[q]gR23dd/'Ug|fn9xfYBٞ[rvI<==P(066fڵ=Zk˗/kF!ƏOPPԩY:uꄎ!!!j.O;wBQa~2pGGRRR{yll,M6㉎& qeĠ/=|\n޼SM3gAAA\|g!~ĉSSS(+++WСCY|9ӦM㧟~EbcciҤ 6DWWT찳‚RRR~WߞGQUBθ5 nf{Aq)SBNٹ>om3-9kٲ%?!11h>3 !={̙3Dxx8[neĈ@z 9  !!ݠTWW~ /-ѣ̙3JӧYd )fff5Y8~_QQ , 11={}v|||l:m"''VXR322"..NkBCCIJJBVb ZhA۶mΌ ,YӧIMMeݘj]%P>xiQT̜9S;.\%K:t͛k?<$%%j*y144W_eŊDEEļy8<5]}{R[=GUyq_\jǮdpJn1dѯUn~1gos7b>&"u'c_MHFf# !sKjժʕ+_ͣL:(_cd˰a믙1c{M5ŋor9֮]YI<==ךR*,_"|}}:u*}| ?LGGٳgψ#ؿ{խ[;;;|}}YjӦMW^E3}t8p K.eرq%M{3335Q wwwXhQ9rHz?/2QQQ,_wO=K fϞ͵k4h'N}tԉ˗/k[ooT*n9ѵkWΝ;ǰaÈb՚+'L@߾}9s&>>>ܾ}+Wbdd>|*&l<Ʉ',oZؚVZ~@+^`ˤ^gvIfWKWXOn]G/^ˍ:er'""%Ko߾nŋ~}ׄB?s!BD3!BD5B!j9B!A$9B!A$9B!A$9B!A$9B!A$9B!A$9B!A$9B!A$9B!AU-_mB!rrrR9MB!5Lk !B !B !B !B !B !B !B !B !B !B !B HrVPP+¹s^aԨQ֭[5^}U\]]+ Z0qD|}}?>.\`޼yѿfϞM~~V?WwjaT~͚5ҿy뭷033nhڵ^}nT/':9קB#2w9W_>Θ֯[3Cu7Ckr̙3Q(*8p `ggӧٳgm۶Z{= B߾};F5NYY.o``|P]wNti\_\_`ƖPK_;RTw#-jhT皜гgO޽{km0`]tOiias>ꎏӧ˂ 4-\l/^Ljj*9 &NV=nb """@ڶmKtt4 >___N<Ɍ3puueΝ(J-[ƱcI&ѹsg 8s ۗ[Jחk׮Ѯ];̙S۩VYt)₿? 626lw}GII GھtR233Yp!lܸ 7o?^~}֮]˞={HOOܜ7|Ç?KOOgDEEQN3f ݻ-[`ccÑ#G8q"/2۷o'887oć~Hǎ숅OR@q5b:9gڐVX5'# c&x6[s|Jk#}vuJ,L )Pw~[z`QIw޸CYY+63ΤP<oKK_jw\X]ʝbe`n#C4 P9t)z.n!w\ *.:qok͘MW*MOĦG.3q`3>y: {d_Ffn!MM4prb&Fzׯ1bsR΋r-89z -Mv}Y;tau|}_x9|y繮9aܸqKKK4y[Vvv6{SNʮ_W_}ssꎏ9Bqq1PXFFFNaa!G׳h"=ʲe~ױضmׯgӦMܹ:0w\8z(̞=,֭[͛qrr$ o6 8J6mА &Tij۶m>}+WR!y֭[o/8pݷoK.wo???ڶm[޽믿βeHNN~0e 7nd…8qdtBxx8}o6| ]ta„ >7Q.%{%cDQ8t_kWwC]R_p %e\MaKۻ>E i$`]ԋB^І)';]-dvQԋC۲M)dZ4`~ħ?^$naybQ.x5!GTgz=;U9zT՚L2^}UmOڵk w'8uqaX`7SNL60rssX?B-aaa( J @nq8880|pRRR(((@TP(EOOcy{{关3f<akk=3gdر 7ߤW^4i҄ٳg?m۶/兝}}޽{LeĈk.x!..yL۶mg֭Zɜ;wƆo7|AiҤ 7o~o;!hAx,Dy?56~Krs&_jS=Xv$ex\ɷUu]#l YftFM:0Ŗڐ_TܛѰbC|Z}z _j@͌zP)C:bTr'; .N~1ϧ2ū%-lM\ܛ>5N+]}Z[lQeüliLFf؊'S[WZ~ٹ>6u06Tbbߟ B|}H hc]ZHFfrb~:W~^Zթ~krbYX0LG;_lEXur xk8wW7|aU΃w'O&==ףfڵk;vL$ӇH܈wRpppXSm۶V8*lllrtt$$$Ҳ>>>DFFJbb"ϟ*2L6m4WVVV$$$`ggJDD Cݛ!CTZ6>>^~֭زchhɓ+-۷o_Μ9իIHHVM-krN{ASkSͶV x׻ZW3Y|B~Bu)eKHSw{V˯ok]%Z\Ri-}PVmh}sqჯ趍(-hmWu 9t)A.6DĦؾ?V<&g7\֯_E2GeĈd̙sf͚::sM-PRRRVzEii)ǏƍxxxŰarQ?v6jԈ09‘#GXv-?7nԌp=Ϗ{ܱ+a6n޽;cƌaȑ|JJJb͚5j^}YYYs)ym\̂!KT27t2()+POڷ;s*!/L E]RV/J]E}SȿVfjрK)w9xVǏɄ'Fuy_*dße<UGY1C @WbefȚ:Wײ!SbXnrTwbVKތ-ρ.IYyGMjj*w[[ o%22ϟLJ0"""HIIaҥ(J5kLMM9}4٨T*fΜ߳Ν;̌3r .]b޼y #f7߰w^Xz5/__gKؕ ] -{"H:6d8x\f!ݚZЁ{/L pb41}6ԩb&n~1?$}LE)RsH##w]B~/05cP{[>"pF6^~~Oٹ>Ζ&r+7sryaեU+*MhPf<ԥȎ$Efv>U|w;˗M6mX|9zzz>{l-ZĠA077˗/WX?P(Yd oٓ>_233i֬Wٹzޘ)J@ vmy5yp ć_Ǡ.-kcs־LҒuz':FLmW\6 hcř,&ǔ5eWK>y{%6Ll;Oj(_7~]؊?^`L`4 /OqTx( zhG[TZڭ~#ʒ-x(?^޼6m!-dy]wɎqT|DG츀ʣ2P2 ch_Ά^wOi>zGLYDDDdWMB񜕕Cd|7wVWk !d::b%N\́Ŭ{Z[Ib&I΄B?ݢ7ڳNjPOAVV lW,dZS! B!*'əB!D "əB!D "əB!D "əB!D "əB!D "əB!D "əB!D "əB!D R?_mB!rrrR9MB!5Lk !B !B !B !B !B !B !B !B !B !B !B 59䣏>W^KZϟ?ȑ#ѣ#G… Z}||puuz}:V<6mZ1q8 \~͚59r乷#33iӦQPP{\~O9z9:r+BR6sf !UQRR'Od4lGGG޽{qqq1vXtttغu+QQQߟdظR嫻ޚr|8ӆ7s<98X3ݚWwXS=iMB#ڵ'N0x` 4_w>VZt(O 0a}חꎏ9Bqq1P|FFFNaa!G׳h"=ʲe~ױضmׯgӦMܹ:0w\8z(̞=,֭[͛qrr$o6 8PLB`ƍ,\'N9ӧYr%!!!чر[qoӧcƌ֭[}oN^ؿ?#GO?%==(~=~8~)3qDJKKYj5bƌL>J1E__%KХK ۟B&N5| C a˖-M)RWT½ndx"Xcۨгd.l cꈈMeovi@BfL܂{/q.*vIavMYɷX*=uta˭ 9:ւsx.)/82|%݊mgL4E%f!Tma^^^ܾ}nݺ䤵MRkԮ][;w4hwϏ`4iRq$'N͍SNQPP&++ߴicܸq|,ƏB֖^{0RԔ֭={QÇ3j( 044DP111űvZ Scĉ$''cdd-&&&̜9Jۨ> Ç3`FAtt44oޜ!C/l2.^H:uؼy3o(OǩS4JR)--SSJ?)nQQQܽ{ٳgQTU:ZV-}]6U[S z6o@݁QkSP`^Ntt@jv>JsĀƖƜwQocEm>~ 0#_DđSYNZؖ}ޔ9[eMּ W-xGr<.2t`]FBc:8CW$HrΝ;w5k~!ׯlkذ!GL2]]]} mrEÙ:uju (OBCdd$nnnDDDлwo PT888hM˶mVsóѪёJIhh(?@3e0JEaa!z]VСC`tЁ޽{kQTk^+..J144^PP@bb"C&fOJQFiʷjJ3QיM~ﮏbկO7{""4*Č{q҇p6WKN+3#M,hi[goз%gor] եBb=ʠV 4Oȸ@k1ңymTi՞6u0ͬMټޮv+OIΌ166f;ddd`n^ᡧڵcذaڵCCCH}dddTwxzz2sL9pfz/`4Iz@IIIWXXBQs <<cȐ!ҽ{woHJJ$44aÆгgO6oILLdҥ\~]>ե_~ /hFSz聥%sARqi,YG333 $..p>>@HTvv6ҌEEECLL +V\`ddD\\Ns8;;3c \¥K7ojCCC222Xd O&55ݻwcjjmX$--7|;vm6n޼Ipp0}fMדꫯb HJJb޼y?^!111\z-ZW_}իWYt3<1nݺuBsۧB$.5cW28v% i2s +72)PbŞK51aaaL0A( wΡCXjݝɓ'T*_~aдiS~G6nHAAW\ۛxbbbܹ3ܹsL0wwwׯϱcN:pQ ֻӣGر#iii_͛7cjj4hqHH+i&mFƍi֬zzzt҅k׮cϡcfooO=駟Xz5 4d-(8pgS}6C0eڋpus~Ɨq>mL :8֣>$͞:Grv+ S}=gS3 XƔY]Lw(:ٹ>]XpV{.49rJouFСs}3+r{|BZzXQ؀Ƴ!*0޳[4P );{8,Y}UwSB'K`B!jI΄B!jB!AdL!L!L!L!L!L!L!L!L!QV`|||uU!/ɩJ7 !B 2)BQHr&BQHr&BQHr&BQHr&BQHr&BQHr&BQHr&BQHr&BQHr&BQHr&BQ<͛L<>}ɧ~JAAf{FFSL͍A7Tw4vԥVپ}{gggQ!C̟?9sTߟhرcuё3gp9ܪ;)eeehdiL5 fl9K^Q ua]=rB繍ٱaMb:y$Z?#ƌS1ӧ3sL.\ȴiHMMeԩۗ~d +SWpp0F ""7x /3h 6mڤ3HMMՕ[n̙3yڵ+>>>DGG_Ə?7|G1x`VXAQQ#Gdڵۗ}ÇW^̞=JcꫯvZСCQO=zy饗pss㭷"66JVrJҥ LWG:u7|ݻզ/_qss?$55ϳ'/"vʧ_uv /)..#oyAq)w_fHR+?]5G~iMkKX ^K ^ro&TwK{nə1...eeel޼WWW& K/y3ȑ#/R"##qwwѣGSTTYhGeٲeXqqql۶׳i&vI;w.VVV=zKKKfϞMVV֭c899`~m8p L2BƍYp!'N @ss=˔)SXr%AAAlذ <Pf̘ɓQ*O= hb8n8[Ø1c*Mݻ?>aaa,[JcRNٶm~~~\SN`:ꫯP=6a};vuV<==_7oF"<uXp#3F[_ uI~1>~k+2s8tGZDl*[[*|X3)֎Ix=v_"ulBg]@\\&L //;wҥKy7Yb_ꋧ{8q(])((͍Çł hܸ1:ubڴi200`( lmmy CPT*5eu놿?7ÇBAAJB.zzzǼypvvm۶uVMҩP(קo߾D-PTmnݺUx+_0`vvv1v988Ӿ}{lmm1b$$$TÇ呝ߟ5k@VV{a鸺ҤI,X@RRǏs> ===8p {X,ܟ='^&fT/'rs&_jS=Xv$ex\c4Gg?_>"_T±+鸷P6҆ukĈ*HH=!Da+V 44˗ӨQ]]]LLL3gJ_Nxx8*קO>DFFFDD{Jƚm۶ׯ?lllrtt$$$Ҳ>>>DFFJbb"ϟGR(,,Zj011x@4h@SVTj%+Ӱag:ޓR_ l/..ɋ˙3gXz5 \|Jc(cccF3^^^ԫWO3zֺukM:uШQ#T*M4ys}_-011!((x]ƕ+Wpvvuw:ӵ99j49*Zۙi1ңymTi42ylmy8r9:iרnr}[Zr&6#!=)w)TR5!c<䬬E{nСfEEE ggU̜98Y$FGG{zGG],^XXBQqs <<גƎK޽OgÆ lܸӽ{wƌȑ#܇ &Edd$f۶m|'X[[WZ9y~Xtt4&LO>+W_U⟩ym\$Mn, ;N%ӫyJ)+\ҧ%|+7s8wmc`C56gp{[7`̀\}"_s\v-{eZ@6mz*jZJֶcL:uꄎ!!!jv T*)sΡP(Fhe]KIIݻ籱4mҏ'::ƍ;k='='-- }}}찳#''UVUiTYUxO꫽=} ɓO<~ĉSSS*ɣRSSYt)NDD5BGGG뢄;wp z1SSS~Gy&ҢE LMMy饗Xx1gΜ!>>ٳgcaaAnݞ<$-- SSS]FRR|T'N! ñ+A[L4Ekm/ul.p&6i SC5xjݞmX ha[RZzNMv^1{,P~ynف4WhN0AQXX!W&##7x+Wj_'k^S*,_"|}}:u*}p (1={61h[.vvvj*MF^h߾=NNN f,]cbddĥK4dԩ( >sLLLx뭷;v,͛7UU׏ɓ'qF^yvŋiٲ;{l]ƠA8q"۷SN\|B U9NbС|___&OL9r$zzz]O;ϵj⥗^bʕ8pKKK|||>|8oWf!*|Xń'$3>I&nAz|u #GOWڷ;c|ǿoӢ5j2hq$7C]:9rݧ/N̙8,Y}UwSB!ğLB!D "əB!D "ӚB!5 !B !B !B !B !B !B !B !B !B ʪ !Be999U&!BD5B!jI΄B!jI΄B!jI΄B!jI΄B!jI΄B!jI΄B!jI΄B!jI΄B!jI΄B!jI΄B!j皜ݼyɓ'ӧO<==O)((O>յ]Ν;͍)SUynvڅsKV}*&""CPANN$$$} Q9{{X3q{ݥYat[أytx/Cfٛ1)}o-9+))aҤI… 9z(˗/`ZJXbfffxxxp),X1cشi̚5S#8p 6T5k8rHu7}}}z->kBzq*g'cJoqƬ~uwgnbĪĒ7ӥ9|ҝn/9r qqq̙3gggڷoرcٻw/tE8wua„ 7(n߾]1q>?=9_ Fk̕9ĥ<4(|0 f[8C=]קKjހ)^-hT64!P>ذauѼSaZZ,\=)SPXX)SZZoM0}ttuuY`浅 ŋIMM?_~APĉ100Ъ֭[ <~ٿ?AAADDDH۶mÇɓ'1cܹRɲe8vyyy8880i$:w_|AXXgΜaxbS3}}}"""[nlٲ#Gg}FVV}e֬YVѰaxٳg4nܘYf`̙t֭ؑ[qpp`ذa 8Hqpp`ɴo1S,] pqqߟ jYZZ}v+233iڴ)&Muܧlٹs'wޥ{L:CCCyXbqqqԭ[#Fk\|7ozjׯ+Tu hkWq*Ik[q.6DE|ZL܂uyo 31w9$oPsΏ`_O,j's!ƚaOn)`QolpcS  ]gRx(Č{Wҷ%/BOWB?2sX"ǮdW6sڭ\~_63{X6Pxi߽q2WZcmf_ox"6u8r)WXoNsR6ukoVttg?v ` ;X}s1P*ڇgk{n#gƸhyf\]]+ ё^zi^355$VXA=_~uHÃ#GP\\ 'SXXѣ)**b,ZGlٲu8mٴi;wC̝;+++=%g&++uֱyf4 o7$44(O 7nd…8qͱϟ?Ͻ{ػw/SLaʕaMBrǶ=889sO?V׿E.]K.,Y0uTGӦMOm۶qiV\IHHH$w<7o>}0fnݺ~پ};)))W^e̘1tܙM61zhVX&~`̘1'''_!If(:ni3)K)9b-~46z3bw+{ڧH]ʄ'23$d\|;*vQԋC۲MRi١g-bݻ]< 'KVR&nݯRظq# M]O;+n!Z;^heIaǮӫyM9nX>1=?$sUThν53_n.|W36=]/bKycmf}]oVZ[S z6o@݁QkSP\%>V`QQ>}ɷ5'׆tűAw~Q nRWm2S-Y>6f@]Jc$Fkxxb5IVXAhh(˗/|w!JJJׯ_<ԩS K.'|{>}͍z* Mbжm[JKK~3F.GGGBBB*-Cdd$$&&ry)aJEaa&OV FF4xAT*)**zl.湞%%%T#O2tP"""0`:twޚiT*/Ř>5mTݏoBB&M6|wN>,{ngZ74Ā1ZəzKJScMyV99kakOڭli\iynDEhT"8|a~C<\HA[[5Ԁ>.KP+(˓ j7r3Uz.AM ZPa*mTOOևgk{YYY-bСC e9Bn`Ȑ!ڵK3mTSxzz2sL9pR===tt? ʣ#K(gKKK?~<7n/// _/))ʊ5kTfiiիW+]KVYVUp5jDXXGȑ#]~7>·;v,{z{w-B}h_x+YZ>‚epryŚJg_QuvɄ'Fuy_Xe0>$72hgW{[urҏ5P*pyT}= e/_amf[3 zh`P炞R 5mюYI/NzO\kײw^V^]at_ÇWx}tؑQFi^Ѻ&ԩ:::Vڵ+P>:RՌx;wBAÆ FF>mѵ޽Ռ<Ott48::OC?H.oooOZZυ 믙3gNuuҿObb"*8;;k藊=i^iٲ%/^G@322aÆ8::r9Ν޾zr~?<.&eҠtm@<&b^fzk1/l=@P,Az:}ZZѩms]XiD_$ԯ'm\͝-=r ]y;>X]JT`{]5 󒡽 z6*Y)qqq|xyyq];yw)))ZS>uޝ 9  !!ݸyե_~ /hFez聥%sARqi,YG[EammM`` qqqWX`_TTĂ HLLdϞ=l߾]Ȉl:"''VX<422"..SNѹsg1cW\ҥK̛7Z]/=-f,YӧOݻ155ֶB]ǯ7|;vm6n޼Ipp0}])gl2Ο?ٳgYb 6X֭[GRRwf˖-cbb+MPɫ]Xռaǩ*cKL|WYԳyL X}, s&a5ɜY-}͌J\jɋRsH##w]B~&Jtt *.51,Vܟ~}nM,01`AX,ͧcW28v%W.:k#٬z n~1gos7b>&"˵L^I`or_Z54#,:ndsJ_|}xx疜8p@s u23*<^>Fŋy79wk׮cT'Z@T|reԩӇ3gV_GGٳgψ#ؿ?|Vubgg/Vbڴi[۷o3}t8p K.eرq%M{333:u* ?z-ƎKkYUb6rHz?/2QQQ,_҄%$$Я_?&OƍyWعs'/e˖|ѧOƏ'[|9dС[ɓ']i=O=iٛx@Yc\J[!.>^W*t_HL  Qv<jrM4߿ fFaʣܾWS*hY1dK>EXEh.R>CN/fP9Lx Ou WƚC0/veRp /~3$3٫%IYy+,ylZ[x" 鈊#;jsSC[{.7y,}:e27RDDDdWM}߻MBr5XGlVrjB$$q3;Wz:[[ !ϙB.k3#mˆȫ,<&K !H5B!jB!A$9B!A$9B!A$9B!A$9B!A$9B!A$9B!A$9B!A$9B!A㫻B!YNNNU*'I!iM!BD3!BD3!BD3!BD3!BD3!BD3!BD3!BD3!BD3!BD3!B*n޼ҥKА~1~x %$$0rH~g}||zkk׮յ?۵kWRx{{W|vv6'O?ͳHnR\#*Ԃ)^-[[g?ݝm79pcNx{E,q T-9+))aҤIXYYݻw;w.%%%k%''GQQ\~ӧӰaCM68`Æ UN<֬YCaa?*9{ W5SVVFzN!WGti{1vn@ Odٿ)/ {nٕ+WcڵԩSc'h.\H *쟜LQQWw\j??Ҙ.k+rjVwLhъ5-9cÆ @GGGKѠAWTԯ_F'fӧOGWW h^[p!,^T>s~ L8znݺ 88DDDmۖh >|83T*__~ j(ߞglE] Hym& nAgKT\7fv>.uZ[M?'{/{eeeǯ̈TĜ- &|"YmeɬWڠyj[N%/JG=c}|bSX\ʜm8w:3K[qI+^fdJJ\CZj ğ]`llyYY7oZ/3dȐJWT2a郯/QQQ-9rb|*622www =z4EEE_EqQ-[mX~=6mbΝtЁsbeeѣGddeen:6oތ&|f0e 7nd…8qͱϟ?Ͻ{ػw/SLaʕa8x k֬|ĉ)--ёz+WCPPL4qgI`` +cԨQ3w\޽{+ѓ2uTȶmcʕ:ueB<"u)yE%+,Ff'25F=KVn n8Y k~ >}T0qIJe^TzA#88JU*waРA;޽???iҤIu ݻp 8uqa ьM6 ???ƍ200`( lmmy c(JMnݺѳgO5* p5 Q(ꢧGLLf{L8B|>}7sssZhJ梢"6oL@@۷`׏SNO?1|>̧~cokAggX[[B||< B$//l,,,ښcoo'j%[u^WnM-ټ`xwF=NAqā;5b߯)N(uuhXO"Z|Ё4E_o7;`nb-lJ˅VOlb,L 63̈؛? [0ѳY-Mt$M |}!22777"""ݻ7T*e۶mYhHHHHe}||$44DΟ?=JRUzqZ&99$Z'T*+\BQQQ)낂quuEOO3f;пܞkkk 5-ZĄ vW\cmll̈#1cqssˋzY|u$V9j4vk͞J$1o%'?Hңĭ>úڳ A;85xdbSO4 }"uy}Oj=,{f joK=_uZU%::`bK$&Č{?sMXhw& :Ty_CC ptt$##cӓ3gρ5kPh$rP~keWXXBQqs <<}Xh>>>Ri<{8ީQT.]o6AAA?CPK)--#>-kre{SZ_TKIU.hJYlx+oqƭw ~W֖;,y cLИn&?G_V>|8 .dذa-=;C CL:Lv39rd^#hE d…bŊS&1m4gʕ;OOOJJJذaCK]bb"l۶ٳgOOOپ}; [n+Bll, \+fϞMbb"999Ʋwޖӧѣ0aK,!99۲l8p #33ekTSSCXX_$bcc[fj*̙Ñ#GHJJ"))޽{畳9iwIKvPXQ=Wn&P^"fǧPx7cmnU׳+z0_li<gʪ퇙‘jrJI:TBN;|?dsgՙ{1;>r]ޜq"F:oÚk׮mYtV۰aV~?3sL իN˶ fg}ƈ#Z0k,f̘ALL VQFO)S0}tƍGdd$O=TDLL >>>\ߟ&NȢE4i ,`޼y/&>> &@\\3fGݝCK/ksꫯRSSCDDsӓM6'ŰaØ6m/>p?O0uTƎj%**v}kͮ]G˩k;OvEDၟ.E:wKv`o,+8$$amddd8GTڿXjk<# @M zXsǃ@STTRDLYJnkuuu.C @DD "3Q813Q813Q81s}ʼnO”vtГܖY_Sv\ו{ 6:юIw~[\ol䭇"gXe )ǷmX &~ʪi71<U]|xG%Gҡ+wMޮp&b)G+jkXfq >>7STQ;pwqⱛe+]Iw1 ^)xo6L%"ZX GĮτFZIc9ڷ1_.ʼnn'^|Gv;\Jmjaܝ jguu0za=J jˉ~]sİL, :0~L\?+V3oMxP[2+yx{wLCc1oowG>-C;X $J\Xn?w" Ζ;h5PRUOVC C۽_'63[zPgHCxpsF!A]C#7busޭ/ڸQPVch{sJh7rXO-!|Z̫wEƺ}Gyr>{a(W1T?fQRYG͙jvwiۍa$}<5|k8KRwY֬3tAAO.nۏu*cVlt(j>6wX3مỦO`~#;~zzGSwu$*%%IKw=A=xpKD-M2vfýW31[#*bޚ42UrΆՐWRߦSTQ4w_} ͙{8XPIbz35=at%v2UpRs˸=Ҹkv&^w%_n-8RR͇?dY_;{ ؖ:1aLl>H@O7g~{ש|{UlL= Ls&xcVC;k a5]mhd?QRUGhG6u/N[msmvL3k?UUKJkdddb W L_,5]5E(™(™(™(!v;NNz9+ [͈ȥ77cc+r1:pMpvЄ0~;""r+//???G"bzg}&`2VPhwH>!튈ȿ???<<sOY9343SDDDDDDDDLDLDDDDDDDDLDLDDDDDDDDLDLDDDDDDDDLDLDDDDݗ}4@i%tEXtdate:create2018-04-12T17:59:42+02:00-%tEXtdate:modify2018-04-12T17:59:42+02:00OIENDB`django-tables2-2.1.1/docs/_static/0000755000175000017500000000000013542204315016226 5ustar josephjosephdjango-tables2-2.1.1/docs/_static/example.png0000644000175000017500000004441213542204315020374 0ustar josephjosephPNG  IHDRL'MiCCPICC ProfilexYgTMͻ,a9眓%'AX% HQQbD@QEA0""(PDPQEDPߝsyN}3Gw0<`F>?FQ8"a ?06 q7&EoCf#"v78 T*=YD.!3DҢ`B~@(5n?#c#X_vBTlR!mA4MiԤȈx6g|~Y Jir{DoE0&H_f'?0 ȨbP?0FNތsa注{?x.naؠX3g#>|qt 93L3B0R/l968Nx!80(@ "CS wh K '@,eb`~R.GxSտٿ??(WZb kĚcJ~ Қ  BϿ[}ഩ0hneO Z6F@́h5mEʴ/7*apqA;XItZHhZ YE( ()+g75xYX ȸY(]/FLb2, ' i@)*@C`V9,C+ B,iAd 9AEA.h7BI:uB=!z _0 f`~XV`#v}8N 6?' H(0J2AmEyQtTj?U:BB&P%4͊B#D14t>]nCGЯs5 Ç`0L"&Sôb11SX,+DƯ'6 Va[ױǍ⨸8\ׄM~IxA gKn0~L`&t[ $AB-0EX&%zDb1XF'~#H"$m#F Αn^( 2 &  3dRj-o p  t d  ^ .d$!T&'4''l)/|RxPxYDRU$KE(QTK4XXWtNLPNlgq-PR[{%%f$$$%H>"KHHUK=JkIKWI2222daY YlC9\\yy#38l>+)z)VT4LQVRRWQ PTyJV5WMWP&vLmLUN}z]{M1M?ͣOشnkcӵh/h\+ۨ;ErKЖ-z"zTzB~'' o E  Œ>+Ӎ[MtLRML-LQ\*^1PHn $tfB} ;5tz{DK$>/3շC`es'btbmuPOlGr:A?2g[ŝ,;v$$&M''JAޕuQ4(?7]4=;}*">y?K)0n]ٓ{,a̡<٫>>ھ\ܵ)=|zApAaE=lp0pȮXx#G/%ƗNٖu*_x\i\rhŪc?w ډ'۪%Kj5 5jjo:PSWz:DS}_fCC#_33M;ʟ=ђw? 6z/j]<{IVmP[R\{hDgN.ݮ O_Rynbwvk ףLϱoߦM7nݺv[;:w:jmqm@}A:ny=l03b:rգ{?u{db,pliӯ-g<F\gϗ~J>j{^'%[M/'VVWl֞GGSͳ y̟FrO$;7dQP! 0 OP<f[ Û$$Efaeb``'ȟ+UG) 6 i{yFŋJ*j4^k.i3l3շ72 5J01)172krjmlg/UAkv_7?'y~zL`k(.t9u[Ȭ<*@Lk9'4qN$Ѥ')-i3"srfaVnl=9E{Cm]Ch>u"b# %JejG5t:QvrO]ÍgN54w2|їڤ#::ۺ\qM[OaLe^ߞ'>`q?p0AĐCaңG=ڳ-/xe<=1zMdʩCL'D`Qu2ϵ_r¾ϫ}.RW+ܫk/rJtB&v@a^8 GE~a18PEL%2x=Y(Cl?9$9}lŽ"EIf Rue1K))j+i+H 1C5vNnŖ<0mFƚ& bf<Xe9)1ns[c.n\=o{]v;'vE@O`mоPE [𶈲Ȥ(є1EqфS;͒ؒ&/dOK֕~ #,):{4rϝG'@X-qKEb#R%e*B*GS+<~5KOշ6{8EcnM,Q85?-7ëYO~M~H\}\F"rRpBahփOȨ=h# QpwDACc9S!ABJ)6/9)<|T]gzL~CNMcRҧdvz)C JBʋ*ujirVv!DkCAS&inf ,[Ykؐl&l[r}Ա)Em p]Oיmޖ>>oMrR'$ׅĄдp鈯Q;̣ 1tX|l\V~rB{bNj}SxR*HLӺ2,33&v>2'{o>\y伅n4,>y8^] JbJcʢˣ*h~G9t):ӶN Τ4]j^n8WtEK =β+W#^g wsĝԻdg N= x0oiїvozxӍ#2gm ^[p  `r]?0Bݠ#Y?pԂzaNXw Ń2B:Po$>DOc1fx$2`سw /şïl '?fEfb8I&ȓ=L*L̇X,{(0%ÚV.~Ôcsۄ-^^y|r MWEE1ĎKIR$?HIWdk+((**+DFV: [^p227icb}Ħ6nݾgkC#)[7wgρmdoGc~LTU3!34H)d=ѤwOhJęb35=3u?[cO^}iyFZ &1/FƍH'V7mlmiyυm}~A!2ua Q1c؄FIRvb 22wd7)>=\Y$P|WX6]U9RqZܩu?=Zϰ55h;vs16N+bW+1^Opýʭw(w-ޏ|;'.On[7b\uyQMuO>GNsW.3ϯ.WsZ0Y-Zʈ>xwOcM8X#CQ>;jm_y Bh|>E ϯ.Vq5N8j48^q&<+ʸh1y¥0`UsB(el|}_2t2)wۧa.dInpE7 w43huQqqn5t &&6cB]X?v4R׷3^>% {ϱ_Dxv-X<q`ңܐ&NͶ 'NPen*߻H>ʯU 裏 MbCnZ쭺}p]#f,g56r1 )e89{ 2[g׎}. ^d>(ת#6vǙ3V<᠚A\EW~!ã \!؆%@\_C9d~𬉣⭇\1m<̤-btbY8Q*>ރlO=8=5Vƅ?8B gܾvR)'hŪ/i? ly%lx-6M#kC#)l氢;%ƺ'רG[} {Ťz+JSj frV>#n }‹A! ~Pav 8A5NQ0Q}klckkhe Y.!ҤÇ}||СC~~~rE*Q_mOYIe)laSaxB_6f &Y˜D i GlC-hdf xL]7q6u| oec m$a=uM;&9ՖtQ(k+h6z6"T~G,vԣK*(`ܣI227,GuL[X~כg|ԾGKB,))G<䢮a0Lb[+φMvcO7#T!Gu_ G5]ǜŬc}&]Rxia9n" &3 ^q@*/a&[ Ҟ ,`UkٗCWMq9y",]6Sq ,z&Q!:eŋn;)oKjy:P-ܪ\݇48s;{9}dɯtjI 7)lX(Iͧa #a5Jn2=#Ng,F Wyz_2?tF%P*w' cdއ?ô]/hs 6tl-!x%,Vda,_hѡy!>9% -KXՇc,R.Tqqa@h@_zz}KC?!u T?),c)=,1VqVfI#Ψjf\USF"3etz]чy%S\2Sn\?чڪtp5g1wUoGĕ?_L n"5R*W*Ohs\mڱT6r XK-U[~`L>&bz]GaJcmkWۅgNzҫ_N2 (8)k»TЖRxlLyB!5IS%tps)`=QA:1#JfہBdʺ](42ZHǍ5]{9bEN0  Xiǟ\BD9Nz&u5Ñ40w>ԊB՝c?YkyF&,skScR8(q_؄5>"t,Nm]p }d>83k9lzt̬:#k3kвjf s0}l f6f u?GnǫβwZotchl!QFB&ARBD29ޤV1EWFrھhmd\ۘ$0ߋV\snӻ~#7@h Xƍfó{nBc=PE{k'(}ݦo) c@-$*4![TKa= *jh3=؛1dڻضg|HU7-b#RaчSc{["L,ě)No!ޛmB'%m)#x.UƩ3 - :>v( /ZW>+'fK 4˥-p LXbj!Q)&*O|neVĬ/Ox3f J~2M%7ffKP±'xgѺNHMyQ'i+sQ́O":Q*pT[mxXu<Øuh'> 蛽Ԏ^k8{LM,MlMYqwr7e-^w[/D2Nq %Lo:{?l:X smSk'▏=-$ ׺v'|^'ލF1Mmw8`<5tmAֱY@u1,&/p n6_DTLT 0S H/ZHTh&BBE@lLf> I (|vYiln oP9 ɒz(%I5!ZHl yBW#r0Xff.-WM/5fbA $U"$b * @\+A-"@- 2*+U4 H A.xMch0xac1Ǫi6$/dt[R@GyoBAiH7)%yH&$FyR@GyoBAi rneiaii%[v~3|A+'~e)w]ZX(;?w]!qPZۓ۔++:[%8jJ x ކP%WMF(WR@.( wAi5i\# #deBν GSg %@ZTȪ R0BVi%%,dO$&-K]\_^pkGՕd''aMK=$J]PrAA.$S\s/ ?7)&..ߪ9㑴2(IV+ܸ0*D"QA1롃#I5wC6/7WuĜT+`WuCU@Ee )5ɠ>,:[* % d\Ȅv]!@ˬ.i4of'($,jT bZKXHq ؈DIFʦT:I ).¥y38Cd2I˰v%ᦻfB;;hqIIZPJ 6d_= kAnCSC6Qr[pV@}f/J"{5+#&+OZu5~?/ jj)]gTQ.fRQ7 ?#nV\ :B%3۾pl-C7gjxph[%h3ZmNaS_6:{n멮ZǢ:jo7`{%Ͷ3N6Jj6]-`vkoG<^y1މ] ,G 3 ]ؤ)M!>%bA}V+z TS5yzo~# bGf󎌽6Qh$V슌_vu6RItQ l{=.2.ycn~B ڂbu upd1SlҪZg;l"N zjg͚IcSg \ׯGp?-g(!J:Y:HhfHf]WHyZ )ܡ#z,#tuj$Qy:]<ܮG/TLG9 ab3`qxJYNi%ej5 #!lfo^LEr\\neܻ; K!ɒ;ju j` Gxٗ`'W&m y ;C('sKMdI%%Ia%=A1+ v=`FކB N6ɪ\0;"" 7ҭ!Ud'%6ѥ5y%aN ڥ%AÇs)PIDhJ?"of ULA [JqBKPlmմNhgB[EZV2ƛf+E סFn=BـƛR2T0AuKzT]Lns}HBR6ˤObAIso 7)(ZUHw/+dЛC /#X4)DvM䑟1/shDeW O6a 視l6b 6jel 6e"SEK*Uqs#֕<'] ߹SCLsUD5Ʃ o@HO6rl!eb-4_Iʼ#Eq$#U=L'[kel fΣMZJKμپ0!(ܼ3ƴ0X h7gu iwVFGhҬ$O EHV#6T@ `V@[Dy[l5b3A@Ha%Edsb  >YOP{+R}t+,uGFr\WW r r mbA@] WDA@C A C" B!h($5aYu!)A^{3uy%'SUZVI|N"3[)+}Q&B}mU[P!^#/)4OsB IDAT9RǦkgwey/0)[LY[ԥpKۮ=,&{* !֖νM-'lZȽi]KFޛ$+"FhX rR.'})K2Q[HݦmR:#3V7җZQD+/:Pzh$}g͠tfS g]MfdOϧ>c)g3k ,,uȤjL4\#5xJȴPEoʋZq+WDT/jhZ >8;ݓ?>*XFqmѡ/yzMϸ:boE}Fq_2A^yW WYާamnF{Q$9EhG Nn1{GJeE!Ccܒw_َ/cҽ *M6nQC~.<5:0n {9bEN`pM;"ĦO=/8># zXcH7v޽1.N{iu?m* ˑ/LXS8|g[ 'S!XOn)$kMkAӄpFטm[L9/veHڽ7y? w׵I+p('cmoc :MyMSX}G9W̄7< },J/9P5jwA~|4qZg>r, cwGCӽy@pf] pR1}"6Ե \Y9_޿xa@._p 'k#]υB>5*Tpa{̠=#:3Q+5uMkGl,ՄV{~<8.NA4qAmr?/t/ff3tһ %B5<@A"23spঐw7wǸSZO3 R35qsAιVNr6ןmۏP`9?MWIWO=9Ux7/wXg'QkGftSvA0)JHˮ =Gp1;֏aۅHc v4Dw]]WHlx55K/H= z=zggv=yݻ]Ẽg0v:rV?:[{SfB=<K*!nyExC"iMUؗ!2Fq*paK%.gDŇrK i s*,E*l_FG*oJ׍dco "|}]7)*$K:xr wLQR?~z'[`@ܪ* 9:\U9lw3fd={miAehLM7ZPt^0gdPH6\kTIJ*$l6Aν7 ,:{jr{5rnxRpܚ8kp_c٢SOMe^X.o_BWǶ.>߶EA b+Z)$sa߁DžE2Q50yfXS jx yJ=K/lk2]OGN2S#*9f܂e8arBO96φ9WxI\}|WӅWi8:QQZPxkkB6Fw<{tf?+yE[wN§?nۘx!dWdV "6n=C]&1 >ن'3u*wlM,<}tK<(! ;[xG@Hn&v' y,=mǗyD.koɞ3FprE`n\{X ^u;H[&ۺu؆XkJ1bxwթIO^-׳UlAI{X)Qd2ZfGwa;'16'_퉶]W4FHı~e[Tņ9rˆ˶zIEx_Y~))K/Vy[»Xϥr'p3FzƽLs?+S=ĂI"&JFX%U5$HCщ..((z0#ny Z&ϘaMr3,Uρ2>~褏BC}ɂ/DEJO͆ضAVFM<=:b )W}P"A}"S\v&!`ꇦra0B+Wx@"k?2ɥ;KJ= &T.DC OG& ?7{ `(xCe)J9QR ӝጇ(*u4 W<#NX-q>wj:a/qO> A:g'EKk/\ܼ})"'l˚;_Fzi6r/z<ڗ!\gѲp[ 0fF͊N(/"|ӰOgxi^ro1ô皏a5N-EX]C鉪#^^吣e_Wa`r _;#T#Ơ8ģYr~aI$/2I3 |.XLgz,&KmxrNM9`ս걤.mjyYÔv'&~ 1dzA@UY}dA̠Bdn]љI1dTUM77u/.G$N\$OFRs:: ;.d&R E@>ՂL6[-2Ar2A@c AMC #H!h,$5ia @\=8)E6 1 HG"  ئ!ԃ rH4 ~;RjT!R R!F#o˃ĽIfP H2,s$X9Z# \z8Ahyeo7h aڕgAB@3n@Byj/b-A@eH @h[ o[E%i6N8IENDB`django-tables2-2.1.1/docs/_static/tutorial.png0000644000175000017500000003213213542204315020600 0ustar josephjosephPNG  IHDRjsBITO IDATx^} \T N^e4GՉ%N5z;I=cqㅚ@FI1!DLj4cƌI&b7 @, `g3kX2jk!\ F0'5s 8 a[oNNwi}ܥ7u!0t} C$!p".#c" DwGF DCI@ܥ mƍ?{wFk׮Pumm˗*͙={1 E=dXL8mϟW(W\!Xyl3bÔhĨ^0}bL]m: 84 6 c|j>\*5JflҶE"~7w@˻o3p NȠrg}VRRbhN8C]IG }`J}w1y7Vﮎah ww/Yl-3g矇Hp$wXP#YRSS׭[˴ٳgE oTO "#v,$rJdU[{.[]hR9gz;:QRѬcp8Ls؁-G7.1y0tp}kU3:6V[n`jdNv"֛"}1Ax?曎(#Ē0% JA(++Z;H}}-ҫ{g꒟k(X4?yMa]h#mh62VXT2]JYJzX1H^8zZǹ ¥K[|k?'s`H=0cMM{Q7$ ,F\R:Yqi z t'w@$I0gywXaQ)+-.=٠8Q$n?T|RŘb0aJTVy $$cVm(ݻC AFBP\$.A|=ݨ<).ӌTU{UDVjTʊ$ǘFQjT3ƊGh˄`f#:prBx߬s jyXW\iD͘aΊ>wcnHUj0FXR1#BPU5mQ[${ۚq9 rSL wK=nZUm  l䊪f5$OuL6t4շTZ!< ry-Q;"vT% w0 }wm# ;^]qbbX'hJwl+ſR9¨I[~@e&._ {&妖Amjo%+Q* %,̮Q/TuhS8nB;ڴ%"nРsUKia͈:z (Tb6jE=tuv@Ʌv Jknc,_h}ZJ*TI1Br$%G,Nnj:]9t2Oǚu&EW.X.@@{b3s9'π; /W\%"ZZZt:B;M0+.NٱåаE, Pdj P87WYvh1sdnB8hOn{k7K:FT ^K^-]\-|>M9+ ݯCm]dݹUQ+EAnvn[ lژR! Q_4A8#B~؁s0Z6̆np i!>frH 4`n j 5q%`P0/mmUZ`)${0}b~8т˿:p𩐕s,c/lkՒi8u]x؈Pp$iIIʼnçVdIJԒ'F֤9vHՉC'BVs5C,Odwo?:!^j#n=je |^<X넛tOѰd4خ%n GA|WMı ( Az; pRֿCXuU-UQ" w@u |&np,jITJ#G%B'!BIT-ԍ/?LTYE Z&0ʧ.IKd5jY  IgIpMQ8ݜx鼻S-HBGE62mXYUUф> jXw@:w 5Pu^@Z ,0mMmD* Ua@QLQCBlgwSFEKP^N-\ee[̮={0r'\ i 9sFO1IfIHŏR=XŕB%bR`ޢ*/.'v*-R`m2IUY,´T fRL&W9mE%ACЁ(T|D##Y@WmiiL g`*AF)TUY@ȩ #r ɂ&k8T!pbm͗\|GrHhP%CFW,.chgWY{5-VU MmWQlյƈ {`3\s\,s ,+NO~ԩUEťIA ~8j;@8:{9.9s :111_5% )\uuӦM'v PI>}:,!Ap!ǎUVy[77=Y׮aw/Ɉ);xE4\ܷ5wO9ٷ O& 7U)>X^ZVir ceg*naԴC|yД'P]i׀FT,{Ҙ$닌3/bX9~{teK+ U5ٳ:*/o?PYyk;LL`LEX0ƸG/:r9=rwDc)OM N@KU]C=u5|yB(aa` jnRI!xXv"?ez8շ/_l0ᆮJi=<[ QS\Wۢ FweCUmWp4v Goܒ!;1qD.>skoGld[uu h(q@{Y]] >.]§/ׂV~ =4a/?YZ)c`$$A@#啪Дg~ˌIMW<^VZzrbL.?R^vg<< qS'2/ uu K_E-~wVD&HůmA3o_UBgK!"T2QCbGc6yzZ~Ⲽ 79855UU*cGYafm`K->fЀ kPF tKfZ<0NJ#{`}t 6wabzdauDBLu zBxiݔI %EZ&}h錾5WA|5M_pԜ%r,ӏ:>@PԔ0-͊:6E#t#r0x|븘dgÏ =#4gUIܞ2?)?6 Gs5Xg^m~+d>SX`6 8kʕ+]e:ƀﳐ(KRzxsq'L` Zphq1.at PBQ9j~-ndvF-$ne(4hSrȸ&F^ $ rS0uuBe,:4F!^c8_V* 8^qDjJ <6ɬ>:h SC ]jh1J-8{9po~\]{. A[D#C|!9[h۟sɛrs}\]P <]-_Aysi$L#do |ɗ˂ oa:J8(^>D%8^JrKQ68-ܒB<=Ci!iԹ輸b$ @GG,Z5Ҟ:yO _Vpw{i7I}l0q 'XsD @,C n:h?o̤E1TJݨ €I(@ $3dF]0 #-|`AwА1QXeG:XD { ov帑'(T3|`ڍ w>]}.ި6&#TB`@1܈ȃ%E <heoa}]xWGU-}86wl8YA!p c$q5JjҥGfT BNDǝxWQQU@q'U' q=M` | )ȟ/;Y+t|s$[nKu$Y5]kN@UUU<  @d d%ő2\Ñ8l#[oфQ U@"&/B+}xʄ@"@x`BW}bb@L(B!WLP B!> eB \@ A BчW0L+>\1A1W & !pEч+&(! D^2! p ^!+P&B/j06JŦ21CC}D Y hKWc&U/*li ^yյsLoEa6< k=%yگfDi9EADo|GfP7Ϣ[2id tY(d<͘|t݉_jjnc= ӵN ?Ь7|UǔWo^|k`//"4$!v"g. y44A?LߧQE-;3Ƕf-l2RD'Bt/r5 [~p{[ZI$E _ffZp8cwY^(/ öGSBg2~ǿě; `c ٳTxD/_Ktj=/IILȧ/|aN@ XO:nK&(͠7Lr7q-3 Ɠ3-";b 4o> T!p"0y8`gV3}g nttړY}Bya'J+Uz,Ìó؁RRڲV 3< 7 g6JtŅNB6#0hd߉M&AB@ےPAWD`71-~QFe='pQr|ccӟ&Dc (!pI%7L?>j_ 퇱LlC/τ7O t'7pf9O zO7ԀV;c,N_P>es/6KĦВk>ի=6{,-1V({I$7IAO|GJ=zjCoھ/=Տ{D ǁn/eKA,|Q6X۟yi;cNxg/ \C*K$7 1׽#qA/Ѿ٠4S`t6--WO:5#xLMD\\A(ZMY)I8|*d0I$^"niOS`yh7:U.7#K4qn${pt*LdCIDAT9\b~6@ѴGb^cc`* }j/htrvi'D|>! L-[v@s_0QDhWa.7(%f|!0 &PK83~Omxڧx:H{ \Tn'%3* \ӿo|Sd1ߌʹ3qLexAېaT/v  1Go2h=E o NG2IX|6f^ ٌg6`5[}YH5}G^mGQ'[@}7H~n.d$NOm0؛3+Puv#7U(* G*F>;H!Ca,V2IgY$H>h( ?Z[aWnX=åk^ L4-O\JsOs~3ٛQTv9ȾT/frĖ>6Jew/JD4Y3 DHM꧲%bY6k5B`x~@)~~ <ݞ/O}M&`vEwU:`؟߽/2zeF8vy$ja+fj,F.Prp)Fshz5_S6>dvCWMrW洹o/z1&%H% Kxb"c?1 E1ʊXͽB1ozLY=W}Э0:3H}xNCXcOwƸRoP@02GP i}_/&>=ݫqtQ(H>?@FldWFUn;ځs s 5!p v11wP >n;ځs s 5!p v11wP +}B&+. v N:uRSoBon݅GC//jBӄx"eߝ7!p##}۴1xE=a[ =>m]|LeLqK?erZx>׉bA7Y-0c[\$;-BEEbau!:>4E[|D؃TdyyAԇ CB|i WY$!5bz 8 rqA(;[W QX QӅVL* [>Pf\ZV'V``HuvQB+ƶ͎k._>weӦ-wm ey 0##Y_$ZOΖWd>R& ɬf&X|$?3#Si3Y5Aw0G=qę3gΟ?ҥ˗/_zڵk---ׯWm˚<'K޸q:NOc &k0[Cy`~E5\۞Jh&#tť~Zg -B*qƙ4lsTZǭYקΝ[_BG|34v4a7lX`sqEyz6ORP`c`&QH$r,emsf.zAV&D 3]J;Ԋ"}ily \Ŗ'yb0FTT |E?O 'Qs׎Ux>X#` XIǤAl'p^3a|7+>eB!`C{ө ht:vܩ.` l6*):o̮Z6)`ۊ!đ Z~rrLiv|ύP0 %[,AY[q/2E#>!-}t~;c[4ʶ+ߟN =yh^1X>lo8](coYYBaA|&Ꭻ++b̄Hv&O`^.*)8>xrRhp{iBp`\5^qqeᾭy[XNj91-j/זo)?#Vrۍ2X3޲>Rq\YǞcx-9D==39K17;Ezv<ߒN`07B`BltL1*!}PH,B!0v@1vj9B#T=B`"c;r-F-zEؽw[[|P{PDcDcޡ#n1>n @#.C-|nc "}v Nr(u]fmwk+=jƌK\7Cn'.*nFf ;/tapm9O۩t҂z=o׻څ->~='A?ӞʕMytzgK߈ h+< iNvDVEyjI32+lhuB܍6y1TI{/~ǩ2''2t4:79coŵfD6L%wcfJ/9,dcʰsKhB<-ư+aC c}6$!06~b8k_UVznaaI`{RPdg%b,9;0ZbTO% <'ZkiaXxޘ8"Dap&+u Y ( J>:xO56ьKg_]>2Z1VO*O[03=Ca_2r$& 0av('^+7QQ,SD $;sc0@/L  dz "Qe˲xEUؒ ;ݕ#o1Nc?,c99/}NZ0m݄`R|,yk7iv<.+ʿ E&)TI9B>9d(;30Y Ŋ,^H%e5#?_&y`m3YYLiQ |4;>#> jC׫鹹Bg{[(!pg"-}TԉQgQVy7·jKYߥOU[AJo=/c l Xq=9+6VAf'X% , @y`NS+fnd{Pa"!z#Kٰ)sr(-8W&GIT-Η W^#-} 3,,&9\G?|06&Sbg469 b-kRذeyq<*Uz$[^-K O(+%819;gxj LQFF'q3I;(BNE[^vQGdhnLaF'}K `|IPlep"7 _ly~Xog_fgdIYIΝ@Bx -!h~i[Lj^͒wY2 Bo %Z-cl[ 12 ϕjyQnN€)2iQT=> /, ^+ V.%Y3t1nFNL?=;dɎg'i1a`H]涠<1gܶ6 =&桬}3孃?_n~8*g[HZj꼸踙k8Wu-kgVZڍn[8ږ8v繺}kZŠ{:ߏoyHf3 n˾˿_eӄ@8+_sq<$G^;E |?^Sd`O&ߘs<yI~}zBY<`$, ɓKT:7c25گPGr\q)DVy@-,"Wo] ݆ v\has5~ټdK8W `d;k,I:CHB` !;} GSesdWl8@E2}FJs6}VmCnI [@q;--Ǘy ا&yQ2BB7ſY&n!O4B><O JC < 8( !O4B><O JC < 8( !O4B><O JC < 8( !O4B><O JC < 8( !O4B><O JC < g#IENDB`django-tables2-2.1.1/docs/_static/tutorial-bootstrap.png0000644000175000017500000007127613542204315022627 0ustar josephjosephPNG  IHDRPi iCCPICC ProfileHT-!RBok`#$cBP++XQ"  lbþAu],PX{wޜs̽9sdTjHre1 @xǗK#6 A_@(  `_*sr|c K1̽6xM\L :d:'azN?CA"K0ƾ/ 0¸Y*o~23MP^$Kyߒa Hld͊P$mrCC,Rŏ0_2^PjmNpU~rq#,ǎlV*V,yѸx^$G8O0yY6*L_( {osUksEqaFJG}ʓT A6*{in*4;Ze/Uy؅]:L^xC$!rA<¹wgI\?VeBW·qrp+P-"lNSJGuYGDjQE V43)|,oX7XN@* tL \ ! fDe>R(bXv. 4q8 2\[p /C? $0]1Gl'AH$IFR D(r)AʑH5r 9\DڑH'҃A>8PhNG3h>ZAJt?ZF/P%cqv8w\ . KppER\%ׄk)q/qD<ax<? _ߋǟw{ >I9BB)pppMxO$DK1L$'"n#.bD%ْIQ$)THBO:EN&}$Fd'r9,!/#O)s'%"̣4QR)TM%՛Gͤ.Qkoi4 ̓6&-.:iZtz }]A_CCoߥe0 ?F #Q8xTW Uի]W{NQ7WW^~XK FOcF1>Mff*}5i,ZZhu1qLSf \bXZͶiMvβf}qT\Em]mŶlyaG˳gG/o5l|[spqvpQ1qc''SMgsbFl''lpDž2eKWW7Wkk[Vw{* =>yzz+k׳wO6yVp|R}~Q|+} [g 8!3pa`s.(4(-X+8>yy$9z/OOJ=~xOHϵSwY[_Y:Wzz_^YVw޵E=zCGݏ{?ji//e_6}` g`@ʓZ6t7{kԩ= !}` `EWK}G 774 336 HOiDOT(Hxr@IDATxǟ , *H"(E 6,*"""EE۰w EDP)JXPPTTT:/ws3{s7-$%P{HHHHH`&pYy    0(A      |HHHHH(      |HHHHH( !{.ٵk޳Gֿ=NŒYP  hcۿ ڹS]SHHHH ($.&,l];e:# HHH@A1 $Ԃv ;=$   (DË$32] oޅHHH #  (۷)oF$@$@$@> |@xH9E`zھm 6 ~I`ݲm6헭J @$@ o K҄8m̪ @2`2'-mʙm@HHRn\V-تBm ^D$@$@$@" @ې d,ac")ئnIHHHH `W[JXlVI:"$@$@$@GA)k@$@$@$@y@ 8.yO^R^fUvTXA~^T,X䕌˕++6:kIŤh"gᏍEӧ7D]1k2WsZ{v-[h UHjש-Փ+%G>Z^*gfyHHH IB34Orȫ/O(XڢPBR@}n1@L ABFv~CBRѢEgVɴ3^88Z?>TR٫zN4%Kd5Pʕ*IsϑڵN"~HrKVyy`S]N:_Wg,X@fϙ#NhdMQ),%VJUUz / Aˈ7%bg҄/$3%᪫rDEv;x9ȣ z xݞ+ȷ{AА|_^= /0V7YU 傋ZeCFoYtٜ۽{o+CT\E=븀aCxgR:Hzrڔ)2k֌+^(?yI#AP[8T#Fk-[ʠW_VۣEΨW_jծc8~ݺ<]^.{ߧxL6cK"mٔ&e`s`>n7C_#nW_)7wfaAX'"C!xvZ# ׯ_\%ϗO+wif̉lG 9'{Tw%&Nғq![[ֿKƎBVieIgaġsR38P O2I:8W.\ȤG!\\ݱ5ke^Ցʗ 6un+V\Z}QU3{]֯`n 9x,[;fMm: #:w,.\`N(-ʗi[qPujlU{V7 `$`{36 ֬^+SMK*\Λý )G׋eg9p`{.] [x/)͓{7/SO>!<oVE۶5k/xi-# 3AC;L:pYJv>\TBMgA:b6uY3gʴv‹.*C/ 6^2[W^G &|;]jzGSLP#d/]D&.;cC9%Y^|+Efs<*En],Mm x?3 ng\ZT{kQjV;l >-Ψkʏ,K`a!D{#}vڛ6m:W&Mzdi 4ȍ-(lI+fTW/)C'2BU\IjZӸiVP~̒ 4,uoZ ®wܠA})U,\ǸWI*:37[䱪WͼCVX(Wh|:t=}-jˏ=L:-tc -cVJMɓ&9|R6/y_GĻBGs>H~8pgmۮFAnyŋ?8PR?}F 3nD>2vak s=OTj_ـx۵sWD9a{ 'jOl1#Ŋ?=p(l10qj{B{aeݺc_㎗CTlUXCP[K^-etwtAcm;n5^D!^jall&? ?_rjش{t'ƈelIu7^Pn=^go h}שƍ{a{ ]~SOtԉ &Y?FmA6vAqs{|q‹RK -s!K/UnC$5_~nݺ˦(g0祢# Ͽt_H;/>H8ۋDMߢy3m =V5̧˰0idΈeloViF0]`~DZ{дY 9SW-k7{]c#< hХRa{#+/F ٯW yGgΉ~@UWv?qNS7yT W>^mqx2}vT=Y99ڈ0_F\ k莾gf#}Ϊ s8ǩ'ok)A_18юo uϨgA%7o5v6vQj"8;蜿9Rn&j>%?-5mX/SU'kߑ!BQ>pv}^2DXod(L0qz~_z2Z[D\{b87K+2Ͽ:;0Dvf' o-j~B7&|0*Cx>OspZ[S n@UW_z*\ŋ[oo~:X0`Iv<}25C03 ,ط5__-YK"x>bLPXc >-Ph;oa'H-l-hݦgҥHa&ʳHG֭[tRn]/_k'|"!|A@UDJ'tV6ctjA69蠽3>|uߴic "ELɮyJ:'x,Yj;kN*E ,k-4l,+W^ǽgLf 4h؛ $ye+M7 W_ueLR8YpO"UvVNDiyw2vQYn[e2iK zvUu) 5zcZ7B5\OB͎{ W4vt#9w #]~GcOxQn\QګV#ktv`]v0:L1jݻ 嚎i sMmG0?pcWl{ymyą.w+g Zv.Z]tyGȾ$Ǫ{׏6j5GyIOG}l x؏1: "c;mPikTLYqPl9iuɥ沿1 n%{Bm : !jծ+ 52Wё~"غɰ&CcKF w?4fA>FkX.Xv-W*PPn=cFЎ69A߸ Qa}Z5>@~Yly}_R'hQhmaAœ*Z 7Ԙݵ6 H2i`PU/ۘН Y A&T¹瞣FKo}xJgzą*|d؍3<}30YA!&g6j  gJs=gCW;*Q:Ðf/QhX6*;f hˋ1hԅӬB!ƺh M aUm[{4,fl nM]6ܦqn RݙC rDH9e9-2y K.m-bCXAܾ;l]ceƣ{}aXzuEKKS%ik*e#.)Z`5/ ٕ,Uʸ*=4>}NW0X0~-W0B#W0Ȯ'`&/2C a=}5>H"I@1A<3 !3:aaI nxEXA\Z`ͪ`o aD$D٧Fܶjm(ͺ2lvDSqG#5OS3"CGa_yNP^#unu(]P^_okA]jYAzH Α?o>\vۤw[۝wҟՁBw‚+;da+ex?HeȯcƳhckBX{L8ywX~n yv4\ P8=c\,IVոҳGu=i#*}5 h[#g;}v&HҾweР,D`.^ (U㮾2aD] Wx PF|G朿nNfzP [#Ի :7<{ĪJ;3 ˓ObԼYS3#&ss!`jX6⸝[xٚ\> ױCZƞ,@fJN`5nA>׮d5\<0Sun0}shc~M%F]MY8]Piw[6B/OF~A$L0Xg G#m,5V,m !1lh]cv - ^]f{ %K2*P0~(yV}c3H k`|D ;b vzBa+~{i'tQ} u>P 7#.׭/sZ+r_3o^E_0JWz̮?XJ,~fgx7pBs+Ѱ0e3:63M'YgͿWKTN~ac6~nnw3@pՑ̉Xh ^{Av3M$!*~{wvu"_{ &*`b]?CnqmG,vhà>/~c0L 'ai<7zU)NnXc l :^)L0{F}u,Wш׵ 5>#тAvUǼz'|Z=z辰;P5q[LVw]ycJk_-q \Ͽ[+&E֬Y~>'oN0xe/0:d}<J/R\z~u$Èm7Uazu-&C<:\QIA|D:оRD@76[hk/UA ;Et]nhƽ`M5V0ŠEL"Y0 c2J!Ph#f_bi 7x5.fTm \9+V?f yQ7H-dl2m P`{!9Ic<f'L(t#3G+TuÆ}"_\.^`[N@5ׄ7"#z)S(ena3P%Ջ5S]\푈[0-]kĈ7{q6J!ҿ IvZ&~Itg%s]^A]g!f4CLFo(|G^rqVL&l\UXDDugta0ta< &|:Pqi2k 4b4A#bUNsMab.P#us &cҢE1NEZc9>ݡB% o)I2~d`2ΝP\[N;]kr&jtwGY\Ìhu;{ 8٠5_yz vp)w/L0Kգ wf!ֶ@>6g`Es{~5:SV0pee'{ſųg6gtV>(`c W,_kNa32݀ٷk[D cc1MtDz&W1>"}y뙧Suu`ta6ЎY*j0YCx)F9養>`-Ҍ}|L&VXANWo8EDwTZ=wE2`Fأ;ٱqܵr[iFl.\Rj[޳詻X3Co;b3sGpvZq`Ui{nys￯W5CwvY?[#g}~] v\tR< uE4U=zy9׋Hpt`Z.@ps(Bme;X (B̔5T=":ZءPBE>zA2Z.nz2l\(Je9!cNwyΦKvm3D6;YG#?p?5yׯlqeZR߾}ջUCS$|N=gNv1CD~G"t3d'0ym=Lފmf1Tnq=4[`XX`Y!T^]u:5a2u'e߮n$b |~X]+ P  ؀O `F+nݕvf{lqvX|8Ir|>O&bA_w2eGN(SVr. (tpxC)ѕDO!5k჆^!wx뱝[B ~W`Nŋtvv m@zs8zrKe|ZBc ی2C&{M]K.VװNW|Xg\U/:rc-HmPVQi*83$KEL_5u̻&<&juq^yˎAc;}id ؅ւfX|\.Ha8 #_;|2jatο"e:q EՊ5ų_e՛9L/>ebGl-/*WP*nc;"c&`E9em\ y|73yW'ofmUGQg>>l{9>F7Cv^Aa_[[ ٠s$ ʸUmR :u>(Ăl}h<*pJ֮^ r5bx:tY yR0@Ef 5JӍJE9 :KPT i?XQ׆W1#n8[\bxermk#n`ܹEvj KIՕ~֩]Kk 7pO>իW3v[ tO;b(vfq Ŕ~^ n/yUVfzU!u"NŚyw,TO՗e7B 6":S xB!7<6cBTfV`" x֮] 7~%J+Uޤ٠2{% scoaU"9/l/|b9tuڠ=Ky)GU#4O^*WXg/Av`@n]BY{$^d_HYP$ѫԨQ=s~-=#dy5$;[ZP93τ49 }'Pv]ld2rj9L0υ ]x"@" i gy@'=sj/^L)bfVZ-J5=f̻H+UݲuKt(p;z ՙx:_ij u #UX_f[qG =(x"(=ϑ@`*-zY95k`:ԮS[꫻?,VVDLYíFU7:L2STH` X W)dq mg[6n?oqOϓ<@%Mqَuvc"HHHHw ''    %@ @  mNbGi"P(3BN$@$@$@$@.gU#U+G_eNﭲ}n9C VmO= )rD;w1 '$Ү9FvEy    !?HA3"PxRaޘD>;WB@8QlG8Dtm(oMEޙӲpX&BQPX?b*`=RΫy\^ĈKX&olhSYI;ܸ>=j7srEU?[wxܝ,pOGX ɁD]|>W6,@@T*IHHH  BFqKcA3|Σ}adӿpk- ppyU#f'VQg&ƜCW&MO>6H$@$@$@$ d`YR]}2т:yylEKڹs/*7V}]O2䫟̾wK- Ś7Tbfwq7( ʚu"N0kL<}uTSL=ըgW2Ҏze,T>B|69$@$@$@$@IHAH -Xm0a4 NiFHpÕO6{aC iyq5hDAYp%5ӎN;Q"c{.F:@$@$@$@$(cPgOrQ"RLdwqd 㰝?U܂̧"D iy#|;#a-吃z:uxAxKs?țSKل>UN]5PခHHHH 5d`Ngt Du̥w^V%h?FO }?SU\h{~&Ve77IDAT <YB6F +RPqj/&sUIxQzqjCfGwbb̎HHHHr@ c`ʇ{wNz%r mayLڡDz :6m6`f EWFO# 'lhmmKU刁HHHH 59nHw=#ɗ߮N`lƝ#$)[PV;uz6`pKS7 bX =qȅ;$@$@$@$@IK W"=ߩ_d 0[0ވT)`Cgzi},װrqO+$wHHHHR@ /" oXNn:wn[{I໕E^~hL+М pԡl;[H!u+놑2տR$@$@$@$@E Wjxg>1~k9ZOxkkd FU/L(=jtW[vCEkS~w1pv#U[erRB-\%O\b/IT"HHHH u9E ڽ=z}ψθz pyn/    !NF%ZtaY|n2+~0zp]i^rJ0@AO޻AX91k1K;?=d!@ @zM}U @@ G @ @  @ ?@ @@pܹs /?ϥUVYf>k֬ꫯ>}_ @ڔhʔ)i„ V~xw}k5<ꨣ`Pu` 4 @42 br?iWlԙ`o7x##^ԫW +t#@ |d`ҤI):J)[/n_'Hof./9 >=OOvX-DA 2o @n IAL7;c_cNv3_xy>5?/y}رG_~&>>|W716l[޽{~:]s5ysG}٧Ѥ):M[otYg  @XF]0I'FN=v G}t_6xbscpg믿>瞵}c%&pw}2E0(v+E@1,j @`Yv`mͿG߃n)t(C@{ॗ^J;C>\<)袋Z:S?O<13&zk0`@ @eY[DV[ehs'/N=XW\1 QtR%DikC,naKz97.qs @,*#@hN,ncNwqG |m({WK.${zrhF4~\>|{Kwyg~C噑k_9C̙3G{j|6ṛAY: @@# t:,\0 <1lذ?`5H$(@ FjC 1P-(FhR`ZkN^{mD` jf8n银z\f֬YicSW_=Ŷ?M>=oo=`PcB `P=~_\0İwS:wQe]Zl.7q*6bjCZ @4@fSW @( 4UU' @ :8 @F  @ S @@# xUՉ @@)N @FD @A'@ ЈA#^Uu"@ A` @hD: @蠀`A0  @4`ЈWU @tP@0  @Q@0hīN @:( tLq @( 4UU' @ :8 @F  @ S @@# xUՉ @@)N @FD @A'@ Ј={bD @@AVJ @hXMҪ @$@ аA^Z#@ P`P @V@0hKb @ R @@  {iU @@AVJ @hXa/ @_@0JI @ + 4U1 @ [)I @a*F @~~+%  @4`аV @/ o$ @ Ҫ @$@ аA^Z#@ P`P @V@0hKb @ R @@  {iU @@AVJ @hXa/ @_`C=3 o߾iРAiVj|w83S6lX>}/ 0 ^  @,K !7}zׯ_^7o^:맛o9M>/ /0opS{,V0jԨ4nܸvp~UW]|ڹ#GLzW;^J/O8m馥V  @,R JiSa@X",DhNw_ۊ)F3`r)- V~'pBEi[n"ґQti{'nIG|WDݻw1cҜ9sg@բ7 @:)Ԃ /o_c8н޻v`_܋`vHGqDkOB4M LҕW^Y1 {gq4k֬S=\6mZ;wn| f̘;ھ;S>~;?b H,qyOOnm^/ygn?Νl @``7;M?[p >쓖[nV9M1EShu]\lʯO?tnFoկ?=H<1%O~o3=6/oۚ nwu/ _Bqz-7|y4gofm.袼^A"r7޸: @%`Pį׿vAO'ļӆ,NDr0O>98 r!y?1)SzAt կ~xi5hOS:蠃8X"$wyycO/I7>v @BKA{D3o={>RZK9,i@mWbҴz*@9(W_~yK . -(ʵ~-zy|5GgbS(%M<9m/t @R ɕoo vytx.wD3D9uYnȻ;u8zhTO0 /r[Zg$JhFp1CkG`QtҤIEq @T`<:)Kt`Ex>'F%~bOi5L.rޟ'iĈm@iF,<0=QGUWZ9;!G_u뭷 @X98*-:o# /R̮\q^{ޭ>FO ~?h.*Ncǎ͡"D}"qW^9 @X 1{c_&A' є-*|k_@Ech.^9 ;;У1bR`wo< E'',(wB>ќ*#BN @R 1Y AZL"$d ʣմ&n2dH,yOϸqRt.ϻ*G3rzAt0ͤd- Î#;|3)iT,^~ 1?e!@ Е] bҲvq1?_\,>&@1AY߾}szA49Ss'&cDQ&:*yXXb>&Iw}w9 `4R,1WCܻwOInZ߈Md?>UP'׿u+ @EKAg~ bw߽Kzo5noƋc 70~f?ϵ @{M<  @tB`Aѷf;: GuGzA|G4G_ON4|FPIch2T$T(Dᨘu9f+=ztQ$ >E3пև @@ ,q0ҳ~t4e_ykn&ꪫ7}-X Ŭ񤡫TLꫧ_Tg0jԨdr' @,2 vS??ĉӦnmՉ @4`Ѝ4f̘` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-tables2.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-tables2.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end django-tables2-2.1.1/docs/Makefile0000644000175000017500000001131313542204315016237 0ustar josephjoseph# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -W SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-tables2.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-tables2.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-tables2" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-tables2" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." spelling: $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling @echo @echo "Spelling check finished, look at the results in " \ "$(BUILDDIR)/spelling/output.txt." django-tables2-2.1.1/docs/pages/0000755000175000017500000000000013542204315015677 5ustar josephjosephdjango-tables2-2.1.1/docs/pages/localization-control.rst0000644000175000017500000000354213542204315022603 0ustar josephjoseph.. _localization-control: Controlling localization ======================== Django-tables2 allows you to define which column of a table should or should not be localized. For example you may want to use this feature in following use cases: * You want to format some columns representing for example numeric values in the given locales even if you don't enable `USE_L10N` in your settings file. * You don't want to format primary key values in your table even if you enabled `USE_L10N` in your settings file. This control is done by using two filter functions in Django's `l10n` library named `localize` and `unlocalize`. Check out Django docs about `localization ` for more information about them. There are two ways of controlling localization in your columns. First one is setting the `~.Column.localize` attribute in your column definition to `True` or `False`. Like so:: class PersonTable(tables.Table): id = tables.Column(accessor="pk", localize=False) class Meta: model = Person .. note:: The default value of the `localize` attribute is `None` which means the formatting of columns is depending on the `USE_L10N` setting. The second way is to define a `~.Table.Meta.localize` and/or `~.Table.Meta.unlocalize` tuples in your tables Meta class (like with `~.Table.Meta.fields` or `~.Table.Meta.exclude`). You can do this like so:: class PersonTable(tables.Table): id = tables.Column(accessor='pk') value = tables.Column(accessor='some_numerical_field') class Meta: model = Person unlocalize = ("id", ) localize = ("value", ) If you define the same column in both `localize` and `unlocalize` then the value of this column will be 'unlocalized' which means that `unlocalize` has higher precedence. django-tables2-2.1.1/docs/pages/table-data.rst0000644000175000017500000000531513542204315020433 0ustar josephjoseph.. _table_data: Populating a table with data ============================ Tables can be created from a range of input data structures. If you have seen the tutorial you will have seen a ``QuerySet`` being used, however any iterable that supports :func:`len` and contains items that exposes key-based access to column values is fine. List of dicts ------------- An an example we will demonstrate using list of dicts. When defining a table it is necessary to declare each column:: import django_tables2 as tables data = [ {"name": "Bradley"}, {"name": "Stevie"}, ] class NameTable(tables.Table): name = tables.Column() table = NameTable(data) QuerySets --------- If you build use tables to display `~django.db.models.query.QuerySet` data, rather than defining each column manually in the table, the `.Table.Meta.model` option allows tables to be dynamically created based on a model:: # models.py from django.contrib.auth import get_user_model from django.db import models class Person(models.Model): first_name = models.CharField(max_length=200) last_name = models.CharField(max_length=200) user = models.ForeignKey(get_user_model(), null=True, on_delete=models.CASCADE) birth_date = models.DateField() # tables.py import django_tables2 as tables class PersonTable(tables.Table): class Meta: model = Person # views.py def person_list(request): table = PersonTable(Person.objects.all()) return render(request, "person_list.html", { "table": table }) This has a number of benefits: - Less repetition - Column headers are defined using the field's `~.models.Field.verbose_name` - Specialized columns are used where possible (e.g. `.DateColumn` for a `~.models.DateField`) When using this approach, the following options might be useful to customize what fields to show or hide: - `~.Table.Meta.sequence` -- reorder columns - `~.Table.Meta.fields` -- specify model fields to *include* - `~.Table.Meta.exclude` -- specify model fields to *exclude* Performance ----------- Django-tables tries to be efficient in displaying big datasets. It tries to avoid converting the `~django.db.models.query.QuerySet` instances to lists by using SQL to slice the data and should be able to handle datasets with 100k records without a problem. However, when performance is degrading, these tips might help: 1. For large datasets, try to use `.LazyPaginator`. 2. Try to strip the table of customizations and check if performance improves. If so, re-add them one by one, checking for performance after each step. This should help to narrow down the source of your performance problems. django-tables2-2.1.1/docs/pages/filtering.rst0000644000175000017500000000232413542204315020415 0ustar josephjoseph.. _filtering: Filtering data in your table ============================ When presenting a large amount of data, filtering is often a necessity. Fortunately, filtering the data in your django-tables2 table is simple with `django-filter `_. The basis of a filtered table is a `SingleTableMixin` combined with a `FilterView` from django-filter:: from django_filters.views import FilterView from django_tables2.views import SingleTableMixin class FilteredPersonListView(SingleTableMixin, FilterView): table_class = PersonTable model = Person template_name = "template.html" filterset_class = PersonFilter The ``FilterSet`` is added to the template context in a ``filter`` variable by default. A basic template rendering the filter (using django-bootstrap3) and table looks like this:: {% load render_table from django_tables2 %} {% load bootstrap3 %} {% if filter %}
{% bootstrap_form filter.form layout='inline' %} {% bootstrap_button 'filter' %} {% endif %} {% render_table table 'django_tables2/bootstrap.html' %} django-tables2-2.1.1/docs/pages/api-reference.rst0000644000175000017500000002702513542204315021144 0ustar josephjoseph.. _api-public: API Reference ============= `.Accessor` (`.A`) ------------------ .. autoclass:: django_tables2.utils.Accessor `.RequestConfig` ---------------- .. autoclass:: django_tables2.config.RequestConfig `.Table` -------- .. autoclass:: django_tables2.tables.Table :members: paginate, as_html, as_values, get_column_class_names, before_render, get_top_pinned_data, get_bottom_pinned_data, get_column_class_names `.Table.Meta` ------------- .. class:: Table.Meta Provides a way to define *global* settings for table, as opposed to defining them for each instance. For example, if you want to create a table of users with their primary key added as a `data-id` attribute on each `
`, You can use the following:: class UsersTable(tables.Table): class Meta: row_attrs = {"data-id": lambda record: record.pk} Which adds the desired ``row_attrs`` to every instance of `UsersTable`, in contrast of defining it at construction time:: table = tables.Table(User.objects.all(), row_attrs={"data-id": lambda record: record.pk}) Some settings are only available in `Table.Meta` and not as an argument to the `~.Table` constructor. .. Note:: If you define a `class Meta` on a child of a table already having a `class Meta` defined, you need to specify the parent's `Meta` class as the parent for the `class Meta` in the child:: class PersonTable(table.Table): class Meta: model = Person exclude = ("email", ) class PersonWithEmailTable(PersonTable): class Meta(PersonTable.Meta): exclude = () All attributes are overwritten if defined in the child's `class Meta`, no merging is attempted. Arguments: attrs (`dict`): Add custom HTML attributes to the table. Allows custom HTML attributes to be specified which will be added to the ``
`` tag of any table rendered via :meth:`.Table.as_html` or the :ref:`template-tags.render_table` template tag. This is typically used to enable a theme for a table (which is done by adding a CSS class to the ``
`` element):: class SimpleTable(tables.Table): name = tables.Column() class Meta: attrs = {"class": "paleblue"} If you supply a a callable as a value in the dict, it will be called at table instantiation an the returned value will be used: Consider this example where each table gets an unique ``"id"`` attribute:: import itertools counter = itertools.count() class UniqueIdTable(tables.Table): name = tables.Column() class Meta: attrs = {"id": lambda: "table_{}".format(next(counter))} .. note:: This functionality is also available via the ``attrs`` keyword argument to a table's constructor. row_attrs (`dict`): Add custom html attributes to the table rows. Allows custom HTML attributes to be specified which will be added to the ```` tag of the rendered table. Optional keyword arguments are `table` and `record`. This can be used to add each record's primary key to each row:: class PersonTable(tables.Table): class Meta: model = Person row_attrs = {"data-id": lambda record: record.pk} # will result in '...' .. note:: This functionality is also available via the ``row_attrs`` keyword argument to a table's constructor. empty_text (str): Defines the text to display when the table has no rows. If the table is empty and ``bool(empty_text)`` is `True`, a row is displayed containing ``empty_text``. This is allows a message such as *There are currently no FOO.* to be displayed. .. note:: This functionality is also available via the ``empty_text`` keyword argument to a table's constructor. show_header (bool): Whether or not to show the table header. Defines whether the table header should be displayed or not, by default, the header shows the column names. .. note:: This functionality is also available via the ``show_header`` keyword argument to a table's constructor. exclude (tuple): Exclude columns from the table. This is useful in subclasses to exclude columns in a parent:: >>> class Person(tables.Table): ... first_name = tables.Column() ... last_name = tables.Column() ... >>> Person.base_columns {'first_name': , 'last_name': } >>> class ForgetfulPerson(Person): ... class Meta: ... exclude = ("last_name", ) ... >>> ForgetfulPerson.base_columns {'first_name': } .. note:: This functionality is also available via the ``exclude`` keyword argument to a table's constructor. However, unlike some of the other `.Table.Meta` options, providing the ``exclude`` keyword to a table's constructor **won't override** the `.Meta.exclude`. Instead, it will be effectively be *added* to it. i.e. you can't use the constructor's ``exclude`` argument to *undo* an exclusion. fields (`tuple`): Fields to show in the table. Used in conjunction with `~.Table.Meta.model`, specifies which fields should have columns in the table. If `None`, all fields are used, otherwise only those named:: # models.py class Person(models.Model): first_name = models.CharField(max_length=200) last_name = models.CharField(max_length=200) # tables.py class PersonTable(tables.Table): class Meta: model = Person fields = ("first_name", ) model (:class:`django.core.db.models.Model`): Create columns from model. A model to inspect and automatically create corresponding columns. This option allows a Django model to be specified to cause the table to automatically generate columns that correspond to the fields in a model. order_by (tuple or str): The default ordering tuple or comma separated str. A hyphen `-` can be used to prefix a column name to indicate *descending* order, for example: `('name', '-age')` or `name,-age`. .. note:: This functionality is also available via the ``order_by`` keyword argument to a table's constructor. sequence (iterable): The sequence of the table columns. This allows the default order of columns (the order they were defined in the Table) to be overridden. The special item `'...'` can be used as a placeholder that will be replaced with all the columns that were not explicitly listed. This allows you to add columns to the front or back when using inheritance. Example:: >>> class Person(tables.Table): ... first_name = tables.Column() ... last_name = tables.Column() ... ... class Meta: ... sequence = ("last_name", "...") ... >>> Person.base_columns.keys() ['last_name', 'first_name'] The ``'...'`` item can be used at most once in the sequence value. If it is not used, every column *must* be explicitly included. For example in the above example, ``sequence = ('last_name', )`` would be **invalid** because neither ``"..."`` or ``"first_name"`` were included. .. note:: This functionality is also available via the ``sequence`` keyword argument to a table's constructor. orderable (bool): Default value for column's *orderable* attribute. If the table and column don't specify a value, a column's ``orderable`` value will fall back to this. This provides an easy mechanism to disable ordering on an entire table, without adding ``orderable=False`` to each column in a table. .. note:: This functionality is also available via the ``orderable`` keyword argument to a table's constructor. template_name (str): The name of template to use when rendering the table. .. note:: This functionality is also available via the ``template_name`` keyword argument to a table's constructor. localize (tuple): Specifies which fields should be localized in the table. Read :ref:`localization-control` for more information. unlocalize (tuple): Specifies which fields should be unlocalized in the table. Read :ref:`localization-control` for more information. Columns ------- `.Column` ~~~~~~~~~ .. autoclass:: django_tables2.columns.Column :members: render, value, order `.BooleanColumn` ~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.BooleanColumn `.CheckBoxColumn` ~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.CheckBoxColumn :members: `.DateColumn` ~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.DateColumn :members: `.DateTimeColumn` ~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.DateTimeColumn :members: `.EmailColumn` ~~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.EmailColumn :members: `.FileColumn` ~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.FileColumn :members: `.JSONColumn` ~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.JSONColumn :members: `.LinkColumn` ~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.LinkColumn :members: `.ManyToManyColumn` ~~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.ManyToManyColumn :members: `.RelatedLinkColumn` ~~~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.RelatedLinkColumn :members: `.TemplateColumn` ~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.TemplateColumn :members: `.URLColumn` ~~~~~~~~~~~~ .. autoclass:: django_tables2.columns.URLColumn :members: Views, view mixins and paginators --------------------------------- `.SingleTableMixin` ~~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.views.SingleTableMixin :members: `.MultiTableMixin` ~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.views.MultiTableMixin :members: `.SingleTableView` ~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.views.SingleTableView :members: get_table, get_table_kwargs `.export.TableExport` ~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.export.TableExport :members: `.export.ExportMixin` ~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.export.ExportMixin :members: `.LazyPaginator` ~~~~~~~~~~~~~~~~ .. autoclass:: django_tables2.paginators.LazyPaginator See :doc:`internal` for internal classes. django-tables2-2.1.1/docs/pages/builtin-columns.rst0000644000175000017500000000153713542204315021563 0ustar josephjoseph.. _builtin-columns: Built-in columns ================ For common use-cases the following columns are included: - `.BooleanColumn` -- renders boolean values - `.Column` -- generic column - `.CheckBoxColumn` -- renders ``checkbox`` form inputs - `.DateColumn` -- date formatting - `.DateTimeColumn` -- ``datetime`` formatting in the local timezone - `.EmailColumn` -- renders ```` tags - `.FileColumn` -- renders files as links - `.JSONColumn` -- renders JSON as an indented string in ``
``
- `.LinkColumn` -- renders ``
`` tags (compose a Django URL) - `.ManyToManyColumn` -- renders a list objects from a `ManyToManyField` - `.RelatedLinkColumn` -- renders ```` tags linking related objects - `.TemplateColumn` -- renders template code - `.URLColumn` -- renders ```` tags (absolute URL) django-tables2-2.1.1/docs/pages/template-tags.rst0000644000175000017500000000404113542204315021177 0ustar josephjoseph.. _template_tags: Template tags ============= .. _template-tags.render_table: render_table ------------ Renders a `~django_tables2.tables.Table` object to HTML and enables as many features in the output as possible. .. sourcecode:: django {% load django_tables2 %} {% render_table table %} {# Alternatively a specific template can be used #} {% render_table table "path/to/custom_table_template.html" %} If the second argument (template path) is given, the template will be rendered with a `.RequestContext` and the table will be in the variable ``table``. .. note:: This tag temporarily modifies the `.Table` object during rendering. A ``context`` attribute is added to the table, providing columns with access to the current context for their own rendering (e.g. `.TemplateColumn`). This tag requires that the template in which it's rendered contains the `~.http.HttpRequest` inside a ``request`` variable. This can be achieved by ensuring the ``TEMPLATES[]['OPTIONS']['context_processors']`` setting contains ``django.template.context_processors.request``. Please refer to the Django documentation for the TEMPLATES-setting_. .. _TEMPLATES-setting: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-TEMPLATES .. _template-tags.querystring: ``querystring`` --------------- A utility that allows you to update a portion of the query-string without overwriting the entire thing. Let's assume we have the query string ``?search=pirates&sort=name&page=5`` and we want to update the ``sort`` parameter: .. sourcecode:: django {% querystring "sort"="dob" %} # ?search=pirates&sort=dob&page=5 {% querystring "sort"="" %} # ?search=pirates&page=5 {% querystring "sort"="" "search"="" %} # ?page=5 {% with "search" as key %} # supports variables as keys {% querystring key="robots" %} # ?search=robots&page=5 {% endwith %} This tag requires the ``django.template.context_processors.request`` context processor, see :ref:`template-tags.render_table`. django-tables2-2.1.1/docs/pages/faq.rst0000644000175000017500000000574313542204315017211 0ustar josephjoseph.. _faq: .. Any code examples in this file should have a corresponding test in tests/test_faq.py FAQ === Some frequently requested questions/examples. All examples assume you import django-tables2 like this:: import django_tables2 as tables How should I fix error messages about the request context processor? -------------------------------------------------------------------- The error message looks something like this:: Tag {% querystring %} requires django.template.context_processors.request to be in the template configuration in settings.TEMPLATES[]OPTIONS.context_processors) in order for the included template tags to function correctly. which should be pretty clear, but here is an example template configuration anyway:: TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": ["templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.contrib.auth.context_processors.auth", "django.template.context_processors.request", "django.template.context_processors.static", ], } } ] How to create a row counter? ---------------------------- You can use ``itertools.counter`` to add row count to a table. Note that in a paginated table, every page's counter will start at zero:: class CountryTable(tables.Table): counter = tables.TemplateColumn("{{ row_counter }}") How to add a footer containing a column total? ---------------------------------------------- Using the `footer`-argument to `~.Column`:: class CountryTable(tables.Table): population = tables.Column( footer=lambda table: sum(x["population"] for x in table.data) ) Or by creating a custom column:: class SummingColumn(tables.Column): def render_footer(self, bound_column, table): return sum(bound_column.accessor.resolve(row) for row in table.data) class Table(tables.Table): name = tables.Column(footer="Total:") population = SummingColumn() Documentation: :ref:`column-footers` .. note :: Your table template must include a block rendering the table footer! Can I use inheritance to build Tables that share features? ---------------------------------------------------------- Yes, like this:: class CountryTable(tables.Table): name = tables.Column() language = tables.Column() A `CountryTable` will show columns `name` and `language`:: class TouristCountryTable(CountryTable): tourist_info = tables.Column() A `TouristCountryTable` will show columns `name`, `language` and `tourist_info`. Overwriting a `Column` attribute from the base class with anything that is not a `Column` will result in removing that Column from the `Table`. For example:: class SimpleCountryTable(CountryTable): language = None A `SimpleCountryTable` will only show column `name`. django-tables2-2.1.1/docs/pages/ordering.rst0000644000175000017500000001122613542204315020244 0ustar josephjosephAlternative column ordering =========================== When using QuerySet data, one might want to show a computed value which is not in the database. In this case, attempting to order the column will cause an exception:: # models.py class Person(models.Model): first_name = models.CharField(max_length=200) family_name = models.CharField(max_length=200) @property def name(self): return "{} {}".format(self.first_name, self.family_name) # tables.py class PersonTable(tables.Table): name = tables.Column() :: >>> table = PersonTable(Person.objects.all()) >>> table.order_by = "name" >>> >>> # will result in: FieldError: Cannot resolve keyword 'name' into field. Choices are: first_name, family_name To prevent this, django-tables2 allows two ways to specify custom ordering: accessors and :meth:`~.order_FOO` methods. .. _order-by-accessors: Ordering by accessors --------------------- You can supply an ``order_by`` argument containing a name or a tuple of the names of the columns the database should use to sort it:: class PersonTable(tables.Table): name = tables.Column(order_by=("first_name", "family_name")) `~.Accessor` syntax can be used as well, as long as they point to a model field. If ordering does not make sense for a particular column, it can be disabled via the ``orderable`` argument:: class SimpleTable(tables.Table): name = tables.Column() actions = tables.Column(orderable=False) .. _table.order_foo: :meth:`table.order_FOO` methods -------------------------------- Another solution for alternative ordering is being able to chain functions on to the original QuerySet. This method allows more complex functionality giving the ability to use all of Django's QuerySet API. Adding a `Table.order_FOO` method (where `FOO` is the name of the column), gives you the ability to chain to, or modify, the original QuerySet when that column is selected to be ordered. The method takes two arguments: `QuerySet`, and `is_descending`. The return must be a tuple of two elements. The first being the QuerySet and the second being a boolean; note that modified QuerySet will only be used if the boolean is `True`. For example, let's say instead of ordering alphabetically, ordering by amount of characters in the first_name is desired. The implementation would look like this: :: # tables.py from django.db.models.functions import Length class PersonTable(tables.Table): name = tables.Column() def order_name(self, queryset, is_descending): queryset = queryset.annotate( length=Length("first_name") ).order_by(("-" if is_descending else ") + "length") return (queryset, True) As another example, presume the situation calls for being able to order by a mathematical expression. In this scenario, the table needs to be able to be ordered by the sum of both the shirts and the pants. The custom column will have its value rendered using :ref:`table.render_FOO`. This can be achieved like this:: # models.py class Person(models.Model): first_name = models.CharField(max_length=200) family_name = models.CharField(max_length=200) shirts = models.IntegerField() pants = models.IntegerField() # tables.py from django.db.models import F class PersonTable(tables.Table): clothing = tables.Column() class Meta: model = Person def render_clothing(self, record): return str(record.shirts + record.pants) def order_clothing(self, queryset, is_descending): queryset = queryset.annotate( amount=F("shirts") + F("pants") ).order_by(("-" if is_descending else ") + "amount") return (queryset, True) Using :meth:`Column.order` on custom columns -------------------------------------------- If you created a custom column, which also requires custom ordering like explained above, you can add the body of your ``order_foo`` method to the order method on your custom column, to allow easier reuse. For example, the `PersonTable` from above could also be defined like this:: class ClothingColumn(tables.Column): def render(self, record): return str(record.shirts + record.pants) def order(self, queryset, is_descending): queryset = queryset.annotate( amount=F("shirts") + F("pants") ).order_by(("-" if is_descending else ") + "amount") return (queryset, True) class PersonTable(tables.Table): clothing = ClothingColumn() class Meta: model = Person django-tables2-2.1.1/docs/pages/generic-mixins.rst0000644000175000017500000000517413542204315021361 0ustar josephjosephClass Based Generic Mixins ========================== Django-tables2 comes with two class based view mixins: `.SingleTableMixin` and `.MultiTableMixin`. A single table using `.SingleTableMixin` ---------------------------------------- `.SingleTableMixin` makes it trivial to incorporate a table into a view or template. The following view parameters are supported: - ``table_class`` –- the table class to use, e.g. ``SimpleTable``, if not specified and ``model`` is provided, a default table will be created on-the-fly. - ``table_data`` (or ``get_table_data()``) -- the data used to populate the table - ``context_table_name`` -- the name of template variable containing the table object - ``table_pagination`` (or ``get_table_pagination``) -- pagination options to pass to `.RequestConfig`. Set ``table_pagination=False`` to disable pagination. - ``get_table_kwargs()`` allows the keyword arguments passed to the ``Table`` constructor. For example:: from django_tables2 import SingleTableView class Person(models.Model): first_name = models.CharField(max_length=200) last_name = models.CharField(max_length=200) class PersonTable(tables.Table): class Meta: model = Person class PersonList(SingleTableView): model = Person table_class = PersonTable The template could then be as simple as: .. sourcecode:: django {% load django_tables2 %} {% render_table table %} Such little code is possible due to the example above taking advantage of default values and `.SingleTableMixin`'s eagerness at finding data sources when one is not explicitly defined. .. note:: You don't have to base your view on `ListView`, you're able to mix `SingleTableMixin` directly. Multiple tables using `.MultiTableMixin` ---------------------------------------- If you need more than one table in a single view you can use `MultiTableMixin`. It manages multiple tables for you and takes care of adding the appropriate prefixes for them. Just define a list of tables in the tables attribute:: from django_tables2 import MultiTableMixin from django.views.generic.base import TemplateView class PersonTablesView(MultiTableMixin, TemplateView): template_name = "multiTable.html" tables = [ PersonTable(qs), PersonTable(qs, exclude=("country", )) ] table_pagination = { "per_page": 10 } In the template, you get a variable `tables`, which you can loop over like this: .. sourcecode:: django {% load django_tables2 %} {% for table in tables %} {% render_table table %} {% endfor %} django-tables2-2.1.1/docs/pages/installation.rst0000644000175000017500000000060413542204315021132 0ustar josephjosephInstallation ============ Django-tables2 is `Available on pypi `_ and can be installed using pip:: pip install django-tables2 After installing, add ``'django_tables2'`` to ``INSTALLED_APPS`` and make sure that ``"django.template.context_processors.request"`` is added to the ``context_processors`` in your template setting ``OPTIONS``. django-tables2-2.1.1/docs/pages/glossary.rst0000644000175000017500000000312413542204315020274 0ustar josephjosephGlossary ======== .. glossary:: accessor Refers to an `.Accessor` object column name The name given to a column. In the follow example, the *column name* is ``age``. .. sourcecode:: python class SimpleTable(tables.Table): age = tables.Column() empty value An empty value is synonymous with "no value". Columns have an ``empty_values`` attribute that contains values that are considered empty. It's a way to declare which values from the database correspond to *null*/*blank*/*missing* etc. order by alias A prefixed column name that describes how a column should impact the order of data within the table. This allows the implementation of how a column affects ordering to be abstracted, which is useful (e.g. in query strings). .. sourcecode:: python class ExampleTable(tables.Table): name = tables.Column(order_by=("first_name", "last_name")) In this example ``-name`` and ``name`` are valid order by aliases. In a query string you might then have ``?order=-name``. table The traditional concept of a table. i.e. a grid of rows and columns containing data. view A Django view. record A single Python object used as the data for a single row. render The act of serializing a `.Table` into HTML. template A Django template. table data An iterable of :term:`records ` that `.Table` uses to populate its rows. django-tables2-2.1.1/docs/pages/column-headers-and-footers.rst0000644000175000017500000001204413542204315023557 0ustar josephjoseph.. _column-headers-and-footers: Customizing headers and footers =============================== By default an header and no footer will be rendered. Adding column headers --------------------- The header cell for each column comes from `~.Column.header`. By default this method returns `~.Column.verbose_name`, falling back to the capitalized attribute name of the column in the table class. When using QuerySet data and a verbose name has not been explicitly defined for a column, the corresponding model field's `verbose_name` will be used. Consider the following: >>> class Region(models.Model): ... name = models.CharField(max_length=200) ... >>> class Person(models.Model): ... first_name = models.CharField(verbose_name="model verbose name", max_length=200) ... last_name = models.CharField(max_length=200) ... region = models.ForeignKey('Region') ... >>> class PersonTable(tables.Table): ... first_name = tables.Column() ... ln = tables.Column(accessor="last_name") ... region_name = tables.Column(accessor="region.name") ... >>> table = PersonTable(Person.objects.all()) >>> table.columns["first_name"].header 'Model Verbose Name' >>> table.columns["ln"].header 'Last Name' >>> table.columns["region_name"].header 'Name' As you can see in the last example (region name), the results are not always desirable when an accessor is used to cross relationships. To get around this be careful to define `.Column.verbose_name`. .. _ordering-class-name: Changing class names for ordered column headers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When a column is ordered in an ascending state there needs to be a way to show it in the interface. django-tables2 does this by adding an `asc` class for ascending or a `desc` class for descending. It should also be known that any orderable column is added with an `orderable` class to the column header. Sometimes there may be a need to change these default classes. On the `attrs` attribute of the table, you can add a `th` key with the value of a dictionary. Within that `th` dictionary, you may add an `_ordering` key also with the value of a dictionary. The `_ordering` element is optional and all elements within it are optional. Inside you can have an `orderable` element, which will change the default `orderable` class name. You can also have `ascending` which will will change the default `asc` class name. And lastly, you can have `descending` which will change the default `desc` class name. Example:: class Table(tables.Table): Meta: attrs = { "th" : { "_ordering": { "orderable": "sortable", # Instead of `orderable` "ascending": "ascend", # Instead of `asc` "descending": "descend" # Instead of `desc` } } } It can also be specified at initialization using the `attrs` for both: table and column:: ATTRIBUTES = { "th" : { "_ordering": { "orderable": "sortable", # Instead of `orderable` "ascending": "ascend", # Instead of `asc` "descending": "descend" # Instead of `desc` } } } table = tables.Table(queryset, attrs=ATTRIBUTES) # or class Table(tables.Table): my_column = tables.Column(attrs=ATTRIBUTES) .. _column-footers: Adding column footers --------------------- By default, no footer will be rendered. If you want to add a footer, define a footer on at least one column. That will make the table render a footer on every view of the table. It is up to you to decide if that makes sense if your table is paginated. Pass `footer`-argument to the `~.Column` constructor. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The simplest case is just passing a `str` as the footer argument to a column:: country = tables.Column(footer="Total:") This will just render the string in the footer. If you need to do more complex things, like showing a sum or an average, you can pass a callable:: population = tables.Column( footer=lambda table: sum(x["population"] for x in table.data) ) You can expect `table`, `column` and `bound_column` as argument. Define `render_footer` on a custom column. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you need the same footer in multiple columns, you can create your own custom column. For example this column that renders the sum of the values in the column:: class SummingColumn(tables.Column): def render_footer(self, bound_column, table): return sum(bound_column.accessor.resolve(row) for row in table.data) Then use this column like so:: class Table(tables.Table): name = tables.Column() country = tables.Column(footer="Total:") population = SummingColumn() .. note:: If you are summing over tables with big datasets, chances are it is going to be slow. You should use some database aggregation function instead. django-tables2-2.1.1/docs/pages/custom-data.rst0000644000175000017500000001330513542204315020654 0ustar josephjoseph.. _accessors: Alternative column data ======================= Various options are available for changing the way the table is :term:`rendered `. Each approach has a different balance of ease-of-use and flexibility. Using `~.Accessors` ------------------- Each column has a 'key' that describes which value to pull from each record to populate the column's cells. By default, this key is just the name given to the column, but it can be changed to allow foreign key traversal or other complex cases. To reduce ambiguity, rather than calling it a 'key', we use the name 'accessor'. Accessors are just dotted paths that describe how an object should be traversed to reach a specific value, for example:: >>> from django_tables2 import A >>> data = {"abc": {"one": {"two": "three"}}} >>> A("abc.one.two").resolve(data) 'three' Dots represent a relationships, and are attempted in this order: 1. Dictionary lookup ``a[b]`` 2. Attribute lookup ``a.b`` 3. List index lookup ``a[int(b)]`` If the resulting value is callable, it is called and the return value is used. .. _table.render_foo: `Table.render_foo` methods -------------------------- To change how a column is rendered, define a ``render_foo`` method on the table for example: `render_row_number()` for a column named `row_number`. This approach is suitable if you have a one-off change that you do not want to use in multiple tables or if you want to combine the data from two columns into one. Supported keyword arguments include: - ``record`` -- the entire record for the row from the :term:`table data` - ``value`` -- the value for the cell retrieved from the :term:`table data` - ``column`` -- the `.Column` object - ``bound_column`` -- the `.BoundColumn` object - ``bound_row`` -- the `.BoundRow` object - ``table`` -- alias for ``self`` This example shows how to render the row number in the first row:: >>> import django_tables2 as tables >>> import itertools >>> >>> class SimpleTable(tables.Table): ... row_number = tables.Column(empty_values=()) ... id = tables.Column() ... age = tables.Column() ... ... def __init__(self, *args, **kwargs): ... super().__init__(*args, **kwargs) ... self.counter = itertools.count() ... ... def render_row_number(self): ... return "Row %d" % next(self.counter) ... ... def render_id(self, value): ... return "<%s>" % value ... >>> table = SimpleTable([{"age": 31, "id": 10}, {"age": 34, "id": 11}]) >>> print(", ".join(map(str, table.rows[0]))) Row 0, <10>, 31 Python's `inspect.getargspec` is used to only pass the arguments declared by the function. This means it's not necessary to add a catch all (``**``) keyword argument. The `render_foo` method can also be used to combine data from two columns into one column. The following example shows how the the value for the `last_name` field is appended to the `name` field using the `render_name` function. Note that `value` is the value in the column and `record` is used to access the values in the `last_name` column:: # models.py class Customers(models.Model): name = models.CharField(max_length=50, null=False, blank=False) last_name = models.CharField(max_length=50, null=False, blank=False) description = models.TextField(blank=True) # tables.py from .models import Customers class CustomerTable(tables.Table): name = tables.Column() description = tables.Column() def render_name(self, value, record): return format_html("{} {}", value, record.last_name) .. important:: `render_foo` methods are *only* called if the value for a cell is determined to be not an :term:`empty value`. When a value is in `.Column.empty_values`, a default value is rendered instead (both `.Column.render` and ``Table.render_FOO`` are skipped). .. important:: `render_foo` methods determine what value is rendered, but which make sorting the column have unexpected results. In those cases, you might want to also define a :ref:`table.order_foo` method. .. _table.value_foo: `Table.value_foo` methods ------------------------- If you want to use `Table.as_values` to export your data, you might want to define a method ``value_foo``, which is analogous to ``render_foo``, but used to render the values rather than the HTML output. Please refer to `.Table.as_values` for an example. .. _subclassing-column: Subclassing `.Column` --------------------- Defining a column subclass allows functionality to be reused across tables. Columns have a `render` method that behaves the same as :ref:`table.render_foo` methods on tables:: >>> import django_tables2 as tables >>> >>> class UpperColumn(tables.Column): ... def render(self, value): ... return value.upper() ... >>> class Example(tables.Table): ... normal = tables.Column() ... upper = UpperColumn() ... >>> data = [{"normal": "Hi there!", ... "upper": "Hi there!"}] ... >>> table = Example(data) >>> # renders to something like this: '''
NormalUpper
Hi there!HI THERE!
''' See :ref:`table.render_foo` for a list of arguments that can be accepted. For complicated columns, you may want to return HTML from the :meth:`~Column.render` method. Make sure to use Django's html formatting functions:: >>> from django.utils.html import format_html >>> >>> class ImageColumn(tables.Column): ... def render(self, value): ... return format_html('', value) ... django-tables2-2.1.1/docs/pages/column-attributes.rst0000644000175000017500000000670413542204315022121 0ustar josephjoseph.. _column-attributes: Column and row attributes ========================= Column attributes ~~~~~~~~~~~~~~~~~ Column attributes can be specified using the `dict` with specific keys. The dict defines HTML attributes for one of more elements within the column. Depending on the column, different elements are supported, however ``th``, ``td``, and ``cell`` are supported universally:: >>> import django_tables2 as tables >>> >>> class SimpleTable(tables.Table): ... name = tables.Column(attrs={"th": {"id": "foo"}}) ... >>> # will render something like this: '{snip}{snip}{snip}' Have a look at each column's API reference to find which elements are supported. If you need to add some extra attributes to column's tags rendered in the footer, use key name ``tf``, as described in section on :ref:`css`. Callables passed in this dict will be called, with optional kwargs ``table``, ``bound_column`` ``record`` and ``value``, with the return value added. For example:: class Table(tables.Table): person = tables.Column(attrs={ "td": { "data-length": lambda value: len(value) } }) will render the ````'s in the tables ```` with a ``data-length`` attribute containing the number of characters in the value. .. note:: The keyword arguments ``record`` and ``value`` only make sense in the context of a row containing data. If you supply a callable with one of these keyword arguments, it will not be executed for the header and footer rows. If you also want to customize the attributes of those tags, you must define a callable with a catchall (``**kwargs``) argument:: def data_first_name(**kwargs): first_name = kwargs.get("value", None) if first_name is None: return "header" else: return first_name class Table(tables.Table): first_name = tables.Column(attrs={ "td": { 'data-first-name': data_first_name } }) This `attrs` can also be defined when subclassing a column, to allow better reuse:: class PersonColumn(tables.Column): attrs = { "td": { "data-first-name": lambda record: record.first_name "data-last-name": lambda record: record.last_name } } def render(self, record): return "{} {}".format(record.first_name, record.last_name) class Table(tables.Table): person = PersonColumn() is equivalent to the previous example. .. _row-attributes: Row attributes ~~~~~~~~~~~~~~ Row attributes can be specified using a dict defining the HTML attributes for the ```` element on each row. By default, class names *odd* and *even* are supplied to the rows, which can be customized using the ``row_attrs`` `.Table.Meta` attribute or as argument to the constructor of `.Table`. String-like values will just be added, callables will be called with optional keyword arguments `record` and `table`, the return value will be added. For example:: class Table(tables.Table): class Meta: model = User row_attrs = { "data-id": lambda record: record.pk } will render tables with the following ```` tag .. sourcecode:: django [...] [...] django-tables2-2.1.1/docs/pages/swapping-columns.rst0000644000175000017500000000141013542204315021733 0ustar josephjoseph.. _swapping-columns: Swapping the position of columns ================================ By default columns are positioned in the same order as they are declared, however when mixing auto-generated columns (via `Table.Meta.model`) with manually declared columns, the column sequence becomes ambiguous. To resolve the ambiguity, columns sequence can be declared via the `.Table.Meta.sequence` option:: class PersonTable(tables.Table): selection = tables.CheckBoxColumn(accessor="pk", orderable=False) class Meta: model = Person sequence = ('selection', 'first_name', 'last_name') The special value ``'...'`` can be used to indicate that any omitted columns should inserted at that location. As such it can be used at most once. django-tables2-2.1.1/docs/pages/query-string-fields.rst0000644000175000017500000000224513542204315022351 0ustar josephjoseph.. _query-string-fields: Query string fields =================== Tables pass data via the query string to indicate ordering and pagination preferences. The names of the query string variables are configurable via the options: - ``order_by_field`` -- default: ``'sort'`` - ``page_field`` -- default: ``"page"`` - ``per_page_field`` -- default: ``"per_page"``, **note:** this field currently is not used by ``{% render_table %}`` Each of these can be specified in three places: - ``Table.Meta.foo`` - ``Table(..., foo=...)`` - ``Table(...).foo = ...`` If you are using multiple tables on a single page, you will want to prefix these fields with a table-specific name, in order to prevent links on one table interfere with those on another table:: def people_listing(request): config = RequestConfig(request) table1 = PeopleTable(Person.objects.all(), prefix="1-") # prefix specified table2 = PeopleTable(Person.objects.all(), prefix="2-") # prefix specified config.configure(table1) config.configure(table2) return render(request, 'people_listing.html', { 'table1': table1, 'table2': table2 }) django-tables2-2.1.1/docs/pages/pagination.rst0000644000175000017500000000325313542204315020565 0ustar josephjoseph.. _pagination: Pagination ========== Pagination is easy, just call :meth:`.Table.paginate` and pass in the current page number:: def people_listing(request): table = PeopleTable(Person.objects.all()) table.paginate(page=request.GET.get("page", 1), per_page=25) return render(request, "people_listing.html", {"table": table}) If you are using `.RequestConfig`, pass pagination options to the constructor:: def people_listing(request): table = PeopleTable(Person.objects.all()) RequestConfig(request, paginate={"per_page": 25}).configure(table) return render(request, "people_listing.html", {"table": table}) If you are using `SingleTableView`, the table will get paginated by default:: class PeopleListView(SingleTableView): table = PeopleTable Disabling pagination ~~~~~~~~~~~~~~~~~~~~ If you are using `SingleTableView` and want to disable the default behavior, set `SingleTableView.table_pagination = False` Lazy pagination ~~~~~~~~~~~~~~~ The default `~django.core.paginators.Paginator` want to count the number of items, which might be an expensive operation for large QuerySets. In those cases, you can use `.LazyPaginator`, which does not perform a count, but also does not know what the total amount of pages will be. The `.LazyPaginator` does this by fetching `n + 1` records where the number of records per page is `n`. If it receives `n` or less records, it knows it is on the last page, preventing rendering of the 'next' button. Usage with `SingleTableView`:: class UserListView(SingleTableView): table_class = UserTable table_data = User.objects.all() paginator_class = LazyPaginator django-tables2-2.1.1/docs/pages/upgrade-changelog.rst0000644000175000017500000000037013542204315022005 0ustar josephjosephUpgrading and change log ======================== Recent versions of django-tables2 have a corresponding git tag for each version released to `pypi `_. .. toctree:: :maxdepth: 1 CHANGELOG.md django-tables2-2.1.1/docs/pages/custom-rendering.rst0000644000175000017500000000721613542204315021724 0ustar josephjosephCustomizing table style ======================= .. _css: CSS --- In order to use CSS to style a table, you'll probably want to add a ``class`` or ``id`` attribute to the ```` element. django-tables2 has a hook that allows arbitrary attributes to be added to the ``
`` tag. .. sourcecode:: python >>> import django_tables2 as tables >>> >>> class SimpleTable(tables.Table): ... id = tables.Column() ... age = tables.Column() ... ... class Meta: ... attrs = {"class": "mytable"} ... >>> table = SimpleTable() >>> # renders to something like this: '
...' You can also specify ``attrs`` attribute when creating a column. ``attrs`` is a dictionary which contains attributes which by default get rendered on various tags involved with rendering a column. You can read more about them in :ref:`column-attributes`. django-tables2 supports three different dictionaries, this way you can give different attributes to column tags in table header (``th``), rows (``td``) or footer (``tf``) .. sourcecode:: python >>> import django_tables2 as tables >>> >>> class SimpleTable(tables.Table): ... id = tables.Column(attrs={"td": {"class": "my-class"}}) ... age = tables.Column(attrs={"tf": {"bgcolor": "red"}}) ... >>> table = SimpleTable() >>> # renders to something like this: '' >>> # and the footer will look like this: ' ... '' .. _available-templates: Available templates ------------------- We ship a couple of different templates: ======================================== ====================================================== Template name Description ======================================== ====================================================== django_tables2/table.html Basic table template (default). django_tables2/bootstrap.html Template using bootstrap 3 structure/classes django_tables2/bootstrap4.html Template using bootstrap 4 structure/classes django_tables2/bootstrap-responsive.html Same as bootstrap, but wrapped in ``.table-responsive`` django_tables2/semantic.html Template using semantic UI ======================================== ====================================================== By default, django-tables2 looks for the ``DJANGO_TABLES2_TEMPLATE`` setting which is ``django_tables2/table.html`` by default. If you use bootstrap 3 for your site, it makes sense to set the default to the bootstrap 3 template:: DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap.html" If you want to specify a custom template for selected tables in your project, you can set a ``template_name`` attribute to your custom ``Table.Meta`` class:: class PersonTable(tables.Table): class Meta: model = Person template_name = "django_tables2/semantic.html" You can also use the ``template_name`` argument to the ``Table`` constructor to override the template for a certain instance:: table = PersonTable(data, template_name="django_tables2/bootstrap-responsive.html") For none of the templates any CSS file is added to the HTML. You are responsible for including the relevant style sheets for a template. .. _custom-template: Custom Template --------------- And of course if you want full control over the way the table is rendered, ignore the built-in generation tools, and instead pass an instance of your `.Table` subclass into your own template, and render it yourself. You should use one of the provided templates as a basis. django-tables2-2.1.1/docs/pages/pinned-rows.rst0000644000175000017500000000317313542204315020702 0ustar josephjoseph.. _pinned_rows: Pinned rows =========== This feature allows one to pin certain rows to the top or bottom of your table. Provide an implementation for one or two of these methods, returning an iterable (QuerySet, list of dicts, list objects) representing the pinned data: * `get_top_pinned_data(self)` - Displays the returned rows on top. * `get_bottom_pinned_data(self)` - Displays the returned rows at the bottom. Pinned rows are not affected by sorting and pagination, they will be present on every page of the table, regardless of ordering. Values will be rendered just like you are used to for normal rows. Example:: class Table(tables.Table): first_name = tables.Column() last_name = tables.Column() def get_top_pinned_data(self): return [ {"first_name": "Janet", "last_name": "Crossen"}, # key "last_name" is None here, so the default value will be rendered. {"first_name": "Trine", "last_name": None} ] .. note:: If you need very different rendering for the bottom pinned rows, chances are you actually want to use column footers: :ref:`column-footers` .. _pinned_row_attributes: Attributes for pinned rows ~~~~~~~~~~~~~~~~~~~~~~~~~~ You can override the attributes used to render the ```` tag of the pinned rows using: ``pinned_row_attrs``. This works exactly like :ref:`row-attributes`. .. note:: By default the ```` tags for pinned rows will get the attribute ``class="pinned-row"``. .. sourcecode:: django [...] [...] django-tables2-2.1.1/docs/pages/internal.rst0000644000175000017500000000327513542204315020254 0ustar josephjoseph============= Internal APIs ============= The items documented here are internal and subject to change. `.BoundColumns` --------------- .. autoclass:: django_tables2.columns.BoundColumns :members: :private-members: :special-members: `.BoundColumn` -------------- .. autoclass:: django_tables2.columns.BoundColumn :members: :private-members: :special-members: `.BoundRows` ------------ .. autoclass:: django_tables2.rows.BoundRows :members: :private-members: :special-members: `.BoundRow` ----------- .. autoclass:: django_tables2.rows.BoundRow :members: :private-members: :special-members: `.BoundPinnedRow` ----------------- .. autoclass:: django_tables2.rows.BoundPinnedRow :members: :private-members: :special-members: `.TableData` ------------ .. autoclass:: django_tables2.tables.TableData :members: :private-members: :special-members: `.utils` -------- .. autoclass:: django_tables2.utils.Sequence :members: :private-members: :special-members: .. autoclass:: django_tables2.utils.OrderBy :members: :private-members: :special-members: .. autoclass:: django_tables2.utils.OrderByTuple :members: :private-members: :special-members: .. autoclass:: django_tables2.utils.Accessor :noindex: :members: :private-members: :special-members: .. autoclass:: django_tables2.utils.AttributeDict :noindex: :members: :private-members: :special-members: .. autofunction:: django_tables2.utils.signature :noindex: .. autofunction:: django_tables2.utils.call_with_appropriate :noindex: .. autofunction:: django_tables2.utils.computed_values :noindex: django-tables2-2.1.1/docs/pages/export.rst0000644000175000017500000001124413542204315017754 0ustar josephjoseph.. _export: Exporting table data ==================== .. versionadded:: 1.8.0 If you want to allow exporting the data present in your django-tables2 tables to various formats, you must install the `tablib `_ package:: pip install tablib Adding ability to export the table data to a class based views looks like this:: import django_tables2 as tables from django_tables2.export.views import ExportMixin from .models import Person from .tables import MyTable class TableView(ExportMixin, tables.SingleTableView): table_class = MyTable model = Person template_name = "django_tables2/bootstrap.html" Now, if you append ``_export=csv`` to the query string, the browser will download a csv file containing your data. Supported export formats are: csv, json, latex, ods, tsv, xls, xlsx, yml To customize the name of the query parameter add an ``export_trigger_param`` attribute to your class. By default, the file will be named ``table.ext``, where ``ext`` is the requested export format extension. To customize this name, add a ``export_name`` attribute to your class. The correct extension will be appended automatically to this value. If you must use a function view, you might use something like this:: from django_tables2.config import RequestConfig from django_tables2.export.export import TableExport from .models import Person from .tables import MyTable def table_view(request): table = MyTable(Person.objects.all()) RequestConfig(request).configure(table) export_format = request.GET.get("_export", None) if TableExport.is_valid_format(export_format): exporter = TableExport(export_format, table) return exporter.response("table.{}".format(export_format)) return render(request, "table.html", { "table": table }) What exactly is exported? ------------------------- The export views use the `.Table.as_values()` method to get the data from the table. Because we often use HTML in our table cells, we need to specify something else for the export to make sense. If you use :ref:`table.render_foo`-methods to customize the output for a column, you should define a :ref:`table.value_foo`-method, returning the value you want to be exported. If you are creating your own custom columns, you should know that each column defines a `value()` method, which is used in `Table.as_values()`. By default, it just calls the `render()` method on that column. If your custom column produces HTML, you should override this method and return the actual value. Including and excluding columns ------------------------------- Some data might be rendered in the HTML version of the table using color coding, but need a different representation in an export format. Use columns with `visible=False` to include columns in the export, but not visible in the regular rendering:: class Table(tables.Table): name = columns.Column(exclude_from_export=True) first_name = columns.Column(visible=False) last_name = columns.Column(visible=False) Certain columns do not make sense while exporting data: you might show images or have a column with buttons you want to exclude from the export. You can define the columns you want to exclude in several ways:: # exclude a column while defining Columns on a table: class Table(tables.Table): name = columns.Column() buttons = columns.TemplateColumn(template_name="...", exclude_from_export=True) # exclude columns while creating the TableExport instance: exporter = TableExport("csv", table, exclude_columns=("image", "buttons")) If you use the ``django_tables2.export.ExportMixin``, add an ``exclude_columns`` attribute to your class:: class TableView(ExportMixin, tables.SingleTableView): table_class = MyTable model = Person template_name = 'django_tables2/bootstrap.html' exclude_columns = ("buttons", ) Generating export URLs ---------------------- You can use the ``export_url`` template tag included with django_tables2 to render a link to export the data as ``csv``:: {% export_url "csv" %} This will make sure any other query string parameters will be preserved, for example in combination when filtering table items. If you want to render more than one button, you could use something like this:: {% for format in view.export_formats %} download .{{ format }} {% endfor %} .. note:: This example assumes you define a list of possible export formats on your view instance in attribute ``export_formats``. django-tables2-2.1.1/docs/pages/table-mixins.rst0000644000175000017500000000160113542204315021023 0ustar josephjosephTable Mixins ============ It's possible to create a mixin for a table that overrides something, however unless it itself is a subclass of `.Table` class variable instances of `.Column` will **not** be added to the class which is using the mixin. Example:: >>> class UselessMixin: ... extra = tables.Column() ... >>> class TestTable(UselessMixin, tables.Table): ... name = tables.Column() ... >>> TestTable.base_columns.keys() ["name"] To have a mixin contribute a column, it needs to be a subclass of `~django_tables2.tables.Table`. With this in mind the previous example *should* have been written as follows:: >>> class UsefulMixin(tables.Table): ... extra = tables.Column() ... >>> class TestTable(UsefulMixin, tables.Table): ... name = tables.Column() ... >>> TestTable.base_columns.keys() ["extra", "name"] django-tables2-2.1.1/docs/pages/reference.rst0000644000175000017500000000013313542204315020364 0ustar josephjosephAPI === .. toctree:: builtin-columns template-tags api-reference internal django-tables2-2.1.1/docs/pages/tutorial.rst0000644000175000017500000001030113542204315020267 0ustar josephjosephTutorial ~~~~~~~~ This is a step-by-step guide to learn how to install and use django-tables2 using Django 2.0 or later. 1. ``pip install django-tables2`` 2. Start a new Django app using `python manage.py startapp tutorial` 3. Add both ``"django_tables2"`` and ``"tutorial"`` to your ``INSTALLED_APPS`` setting in ``settings.py``. Now, add a model to your ``tutorial/models.py``:: # tutorial/models.py class Person(models.Model): name = models.CharField(max_length=100, verbose_name="full name") Create the database tables for the newly added model:: $ python manage.py makemigrations tutorial $ python manage.py migrate tutorial Add some data so you have something to display in the table:: $ python manage.py shell >>> from tutorial.models import Person >>> Person.objects.bulk_create([Person(name="Jieter"), Person(name="Bradley")]) [, ] Now use a generic ``ListView`` to pass a ``Person`` QuerySet into a template. Note that the context name used by `ListView` is `object_list` by default:: # tutorial/views.py from django.views.generic import ListView from .models import Person class PersonListView(ListView): model = Person template_name = 'tutorial/people.html' Add the view to your ``urls.py``:: # urls.py from django.urls import path from django.contrib import admin from tutorial.views import PersonListView urlpatterns = [ path("admin/", admin.site.urls), path("people/", PersonListView.as_view()) ] Finally, create the template:: {# tutorial/templates/tutorial/people.html #} {% load render_table from django_tables2 %} List of persons {% render_table object_list %} You should be able to load the page in the browser (http://localhost:8000/people/ by default), you should see: .. figure:: /_static/tutorial.png :align: center :alt: An example table rendered using django-tables2 This view supports pagination and ordering by default. While simple, passing a QuerySet directly to ``{% render_table %}`` does not allow for any customization. For that, you must define a custom `.Table` class:: # tutorial/tables.py import django_tables2 as tables from .models import Person class PersonTable(tables.Table): class Meta: model = Person template_name = "django_tables2/bootstrap.html" fields = ("name", ) You will then need to instantiate and configure the table in the view, before adding it to the context:: # tutorial/views.py from django_tables2 import SingleTableView from .models import Person from .tables import PersonTable class PersonListView(SingleTableView): model = Person table_class = PersonTable template_name = 'tutorial/people.html' Rather than passing a QuerySet to ``{% render_table %}``, instead pass the table instance:: {# tutorial/templates/tutorial/people.html #} {% load render_table from django_tables2 %} List of persons {% render_table table %} This results in a table rendered with the bootstrap3 style sheet: .. figure:: /_static/tutorial-bootstrap.png :align: center :alt: An example table rendered using django-tables2 with the bootstrap template At this point you have only changed the columns rendered in the table and the template. There are several topic you can read into to further customize the table: - Table data - :ref:`Populating the table with data `, - :ref:`Filtering table data ` - Customizing the rendered table - :ref:`Headers and footers ` - :ref:`pinned_rows` - :ref:`api-public` If you think you don't have a lot customization to do and don't want to make a full class declaration use ``django_tables2.tables.table_factory``. django-tables2-2.1.1/docs/requirements.txt0000644000175000017500000000016213542204315020063 0ustar josephjoseph-r ../requirements/common.pip Sphinx==2.2.0 sphinx_rtd_theme recommonmark django sphinxcontrib-spelling pyenchant django-tables2-2.1.1/.pre-commit-config.yaml0000644000175000017500000000017413542204315020133 0ustar josephjosephrepos: - repo: https://github.com/ambv/black rev: stable hooks: - id: black language_version: python3.6 django-tables2-2.1.1/README.md0000644000175000017500000000527513542204315015140 0ustar josephjoseph# django-tables2 - An app for creating HTML tables [![Latest PyPI version](https://badge.fury.io/py/django-tables2.svg)](https://pypi.python.org/pypi/django-tables2) [![Travis CI](https://travis-ci.org/jieter/django-tables2.svg?branch=master)](https://travis-ci.org/jieter/django-tables2) [![Any color you like](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) django-tables2 simplifies the task of turning sets of data into HTML tables. It has native support for pagination and sorting. It does for HTML tables what `django.forms` does for HTML forms. e.g. - Available on pypi as [django-tables2](https://pypi.python.org/pypi/django-tables2) - Tested against currently supported versions of Django [and supported python 3 versions Django supports](https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django) No python 2.7 support anymore (see [Travis CI](https://travis-ci.org/jieter/django-tables2) - [Documentation on readthedocs.org](https://django-tables2.readthedocs.io/en/latest/) - [Bug tracker](http://github.com/jieter/django-tables2/issues) Features: - Any iterable can be a data-source, but special support for Django `QuerySets` is included. - The builtin UI does not rely on JavaScript. - Support for automatic table generation based on a Django model. - Supports custom column functionality via subclassing. - Pagination. - Column based table sorting. - Template tag to enable trivial rendering to HTML. - Generic view mixin. ![An example table rendered using django-tables2](https://cdn.rawgit.com/jieter/django-tables2/master/docs/img/example.png) ![An example table rendered using django-tables2 and bootstrap theme](https://cdn.rawgit.com/jieter/django-tables2/master/docs/img/bootstrap.png) ![An example table rendered using django-tables2 and semantic-ui theme]( https://cdn.rawgit.com/jieter/django-tables2/master/docs/img/semantic.png) ## Example Start by adding `django_tables2` to your `INSTALLED_APPS` setting like this: ```python INSTALLED_APPS = ( ..., "django_tables2", ) ``` Creating a table for a model `Simple` is as simple as: ```python import django_tables2 as tables class SimpleTable(tables.Table): class Meta: model = Simple ``` This would then be used in a view: ```python class TableView(tables.SingleTableView): table_class = SimpleTable queryset = Simple.objects.all() template_name = "simple_list.html" ``` And finally in the template: ``` {% load django_tables2 %} {% render_table table %} ``` This example shows one of the simplest cases, but django-tables2 can do a lot more! Check out the [documentation](https://django-tables2.readthedocs.io/en/latest/) for more details. django-tables2-2.1.1/django_tables2/0000755000175000017500000000000013542204315016526 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/config.py0000644000175000017500000000456713542204315020361 0ustar josephjosephfrom django.core.paginator import EmptyPage, PageNotAnInteger class RequestConfig: """ A configurator that uses request data to setup a table. A single RequestConfig can be used for multiple tables in one view. Arguments: paginate (dict or bool): Indicates whether to paginate, and if so, what default values to use. If the value evaluates to `False`, pagination will be disabled. A `dict` can be used to specify default values for the call to `~.tables.Table.paginate` (e.g. to define a default `per_page` value). A special *silent* item can be used to enable automatic handling of pagination exceptions using the following logic: - If `~django.core.paginator.PageNotAnInteger` is raised, show the first page. - If `~django.core.paginator.EmptyPage` is raised, show the last page. For example, to use `~.LazyPaginator`:: RequestConfig(paginate={"paginator_class": LazyPaginator}).configure(table) """ def __init__(self, request, paginate=True): self.request = request self.paginate = paginate def configure(self, table): """ Configure a table using information from the request. Arguments: table (`~.Table`): table to be configured """ order_by = self.request.GET.getlist(table.prefixed_order_by_field) if order_by: table.order_by = order_by if self.paginate: if hasattr(self.paginate, "items"): kwargs = dict(self.paginate) else: kwargs = {} # extract some options from the request for arg in ("page", "per_page"): name = getattr(table, "prefixed_%s_field" % arg) try: kwargs[arg] = int(self.request.GET[name]) except (ValueError, KeyError): pass silent = kwargs.pop("silent", True) if not silent: table.paginate(**kwargs) else: try: table.paginate(**kwargs) except PageNotAnInteger: table.page = table.paginator.page(1) except EmptyPage: table.page = table.paginator.page(table.paginator.num_pages) return table django-tables2-2.1.1/django_tables2/views.py0000644000175000017500000001745713542204315020253 0ustar josephjosephfrom itertools import count from django.core.exceptions import ImproperlyConfigured from django.views.generic.list import ListView from . import tables from .config import RequestConfig class TableMixinBase: """ Base mixin for the Single- and MultiTable class based views. """ context_table_name = "table" table_pagination = None def get_context_table_name(self, table): """ Get the name to use for the table's template variable. """ return self.context_table_name def get_table_pagination(self, table): """ Return pagination options passed to `.RequestConfig`: - True for standard pagination (default), - False for no pagination, - a dictionary for custom pagination. `ListView`s pagination attributes are taken into account, if `table_pagination` does not define the corresponding value. Override this method to further customize pagination for a `View`. """ paginate = self.table_pagination if paginate is False: return False paginate = {} if getattr(self, "paginate_by", None) is not None: paginate["per_page"] = self.paginate_by if hasattr(self, "paginator_class"): paginate["paginator_class"] = self.paginator_class if getattr(self, "paginate_orphans", 0) != 0: paginate["orphans"] = self.paginate_orphans # table_pagination overrides any MultipleObjectMixin attributes if self.table_pagination: paginate.update(self.table_pagination) # we have no custom pagination settings, so just use the default. if not paginate and self.table_pagination is None: return True return paginate class SingleTableMixin(TableMixinBase): """ Adds a Table object to the context. Typically used with `.TemplateResponseMixin`. Attributes: table_class: subclass of `.Table` table_data: data used to populate the table, any compatible data source. context_table_name(str): name of the table's template variable (default: 'table') table_pagination (dict): controls table pagination. If a `dict`, passed as the *paginate* keyword argument to `.RequestConfig`. As such, any Truthy value enables pagination. (default: enable pagination). The `dict` can be used to specify values for arguments for the call to `~.tables.Table.paginate`. If you want to use a non-standard paginator for example, you can add a key `paginator_class` to the dict, containing a custom `Paginator` class. This mixin plays nice with the Django's ``.MultipleObjectMixin`` by using ``.get_queryset`` as a fall back for the table data source. """ table_class = None table_data = None def get_table_class(self): """ Return the class to use for the table. """ if self.table_class: return self.table_class if self.model: return tables.table_factory(self.model) raise ImproperlyConfigured( "You must either specify {0}.table_class or {0}.model".format(type(self).__name__) ) def get_table(self, **kwargs): """ Return a table object to use. The table has automatic support for sorting and pagination. """ table_class = self.get_table_class() table = table_class(data=self.get_table_data(), **kwargs) return RequestConfig(self.request, paginate=self.get_table_pagination(table)).configure( table ) def get_table_data(self): """ Return the table data that should be used to populate the rows. """ if self.table_data is not None: return self.table_data elif hasattr(self, "object_list"): return self.object_list elif hasattr(self, "get_queryset"): return self.get_queryset() klass = type(self).__name__ raise ImproperlyConfigured( "Table data was not specified. Define {}.table_data".format(klass) ) def get_table_kwargs(self): """ Return the keyword arguments for instantiating the table. Allows passing customized arguments to the table constructor, for example, to remove the buttons column, you could define this method in your View:: def get_table_kwargs(self): return { 'exclude': ('buttons', ) } """ return {} def get_context_data(self, **kwargs): """ Overridden version of `.TemplateResponseMixin` to inject the table into the template's context. """ context = super().get_context_data(**kwargs) table = self.get_table(**self.get_table_kwargs()) context[self.get_context_table_name(table)] = table return context class SingleTableView(SingleTableMixin, ListView): """ Generic view that renders a template and passes in a `.Table` instances. Mixes ``.SingleTableMixin`` with ``django.views.generic.list.ListView``. """ class MultiTableMixin(TableMixinBase): """ Add a list with multiple Table object's to the context. Typically used with `.TemplateResponseMixin`. The `tables` attribute must be either a list of `.Table` instances or classes extended from `.Table` which are not already instantiated. In that case, `get_tables_data` must be able to return the tables data, either by having an entry containing the data for each table in `tables`, or by overriding this method in order to return this data. Attributes: tables: list of `.Table` instances or list of `.Table` child objects. tables_data: if defined, `tables` is assumed to be a list of table classes which will be instantiated with the corresponding item from this list of `.TableData` instances. table_prefix(str): Prefix to be used for each table. The string must contain one instance of `{}`, which will be replaced by an integer different for each table in the view. Default is 'table_{}-'. context_table_name(str): name of the table's template variable (default: 'tables') .. versionadded:: 1.2.3 """ tables = None tables_data = None table_prefix = "table_{}-" # override context table name to make sense in a multiple table context context_table_name = "tables" def get_tables(self): """ Return an array of table instances containing data. """ if self.tables is None: klass = type(self).__name__ raise ImproperlyConfigured("No tables were specified. Define {}.tables".format(klass)) data = self.get_tables_data() if data is None: return self.tables if len(data) != len(self.tables): klass = type(self).__name__ raise ImproperlyConfigured("len({}.tables_data) != len({}.tables)".format(klass, klass)) return list(Table(data[i]) for i, Table in enumerate(self.tables)) def get_tables_data(self): """ Return an array of table_data that should be used to populate each table """ return self.tables_data def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) tables = self.get_tables() # apply prefixes and execute requestConfig for each table table_counter = count() for table in tables: table.prefix = table.prefix or self.table_prefix.format(next(table_counter)) RequestConfig(self.request, paginate=self.get_table_pagination(table)).configure(table) context[self.get_context_table_name(table)] = list(tables) return context django-tables2-2.1.1/django_tables2/utils.py0000644000175000017500000004300713542204315020244 0ustar josephjosephimport inspect from collections import OrderedDict from functools import total_ordering from itertools import chain from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils.html import format_html_join class Sequence(list): """ Represents a column sequence, e.g. ``('first_name', '...', 'last_name')`` This is used to represent `.Table.Meta.sequence` or the `.Table` constructors's *sequence* keyword argument. The sequence must be a list of column names and is used to specify the order of the columns on a table. Optionally a '...' item can be inserted, which is treated as a *catch-all* for column names that are not explicitly specified. """ def expand(self, columns): """ Expands the ``'...'`` item in the sequence into the appropriate column names that should be placed there. arguments: columns (list): list of column names. returns: The current instance. raises: `ValueError` if the sequence is invalid for the columns. """ ellipses = self.count("...") if ellipses > 1: raise ValueError("'...' must be used at most once in a sequence.") elif ellipses == 0: self.append("...") # everything looks good, let's expand the "..." item columns = list(columns) # take a copy and exhaust the generator head = [] tail = [] target = head # start by adding things to the head for name in self: if name == "...": # now we'll start adding elements to the tail target = tail continue target.append(name) if name in columns: columns.pop(columns.index(name)) self[:] = chain(head, columns, tail) return self class OrderBy(str): """ A single item in an `.OrderByTuple` object. This class is essentially just a `str` with some extra properties. """ QUERYSET_SEPARATOR = "__" @property def bare(self): """ Returns: `.OrderBy`: the bare form. The *bare form* is the non-prefixed form. Typically the bare form is just the ascending form. Example: ``age`` is the bare form of ``-age`` """ return OrderBy(self[1:]) if self[:1] == "-" else self @property def opposite(self): """ Provides the opposite of the current sorting direction. Returns: `.OrderBy`: object with an opposite sort influence. Example:: >>> order_by = OrderBy('name') >>> order_by.opposite '-name' """ return OrderBy(self[1:]) if self.is_descending else OrderBy("-" + self) @property def is_descending(self): """ Returns `True` if this object induces *descending* ordering. """ return self.startswith("-") @property def is_ascending(self): """ Returns `True` if this object induces *ascending* ordering. """ return not self.is_descending def for_queryset(self): """ Returns the current instance usable in Django QuerySet's order_by arguments. """ return self.replace(Accessor.SEPARATOR, OrderBy.QUERYSET_SEPARATOR) class OrderByTuple(tuple): """ Stores ordering as (as `.OrderBy` objects). The `~.Table.order_by` property is always converted to an `.OrderByTuple` object. This class is essentially just a `tuple` with some useful extras. Example:: >>> x = OrderByTuple(('name', '-age')) >>> x['age'] '-age' >>> x['age'].is_descending True >>> x['age'].opposite 'age' """ def __new__(cls, iterable): transformed = [] for item in iterable: if not isinstance(item, OrderBy): item = OrderBy(item) transformed.append(item) return super().__new__(cls, transformed) def __str__(self): return ",".join(self) def __contains__(self, name): """ Determine if a column has an influence on ordering. Example:: >>> x = OrderByTuple(('name', )) >>> 'name' in x True >>> '-name' in x True Arguments: name (str): The name of a column. (optionally prefixed) Returns: bool: `True` if the column with `name` influences the ordering. """ name = OrderBy(name).bare for order_by in self: if order_by.bare == name: return True return False def __getitem__(self, index): """ Allows an `.OrderBy` object to be extracted via named or integer based indexing. When using named based indexing, it's fine to used a prefixed named:: >>> x = OrderByTuple(('name', '-age')) >>> x[0] 'name' >>> x['age'] '-age' >>> x['-age'] '-age' Arguments: index (int): Index to query the ordering for. Returns: `.OrderBy`: for the ordering at the index. """ if isinstance(index, str): for order_by in self: if order_by == index or order_by.bare == index: return order_by raise KeyError return super().__getitem__(index) @property def key(self): accessors = [] reversing = [] for order_by in self: accessors.append(Accessor(order_by.bare)) reversing.append(order_by.is_descending) @total_ordering class Comparator: def __init__(self, obj): self.obj = obj def __eq__(self, other): for accessor in accessors: a = accessor.resolve(self.obj, quiet=True) b = accessor.resolve(other.obj, quiet=True) if not a == b: return False return True def __lt__(self, other): for accessor, reverse in zip(accessors, reversing): a = accessor.resolve(self.obj, quiet=True) b = accessor.resolve(other.obj, quiet=True) if a == b: continue if reverse: a, b = b, a # The rest of this should be refactored out into a util # function 'compare' that handles different types. try: return a < b except TypeError: # If the truth values differ, it's a good way to # determine ordering. if bool(a) is not bool(b): return bool(a) < bool(b) # Handle comparing different types, by falling back to # the string and id of the type. This at least groups # different types together. a_type = type(a) b_type = type(b) return (repr(a_type), id(a_type)) < (repr(b_type), id(b_type)) return False return Comparator def get(self, key, fallback): """ Identical to `__getitem__`, but supports fallback value. """ try: return self[key] except (KeyError, IndexError): return fallback @property def opposite(self): """ Return version with each `.OrderBy` prefix toggled:: >>> order_by = OrderByTuple(('name', '-age')) >>> order_by.opposite ('-name', 'age') """ return type(self)((o.opposite for o in self)) class Accessor(str): """ A string describing a path from one object to another via attribute/index accesses. For convenience, the class has an alias `.A` to allow for more concise code. Relations are separated by a ``.`` character. """ SEPARATOR = "." ALTERS_DATA_ERROR_FMT = "Refusing to call {method}() because `.alters_data = True`" LOOKUP_ERROR_FMT = ( "Failed lookup for key [{key}] in {context}, when resolving the accessor {accessor}" ) def resolve(self, context, safe=True, quiet=False): """ Return an object described by the accessor by traversing the attributes of *context*. Lookups are attempted in the following order: - dictionary (e.g. ``obj[related]``) - attribute (e.g. ``obj.related``) - list-index lookup (e.g. ``obj[int(related)]``) Callable objects are called, and their result is used, before proceeding with the resolving. Example:: >>> x = Accessor('__len__') >>> x.resolve('brad') 4 >>> x = Accessor('0.upper') >>> x.resolve('brad') 'B' Arguments: context : The root/first object to traverse. safe (bool): Don't call anything with `alters_data = True` quiet (bool): Smother all exceptions and instead return `None` Returns: target object Raises: TypeError`, `AttributeError`, `KeyError`, `ValueError` (unless `quiet` == `True`) """ try: current = context for bit in self.bits: try: # dictionary lookup current = current[bit] except (TypeError, AttributeError, KeyError): try: # attribute lookup current = getattr(current, bit) except (TypeError, AttributeError): try: # list-index lookup current = current[int(bit)] except ( IndexError, # list index out of range ValueError, # invalid literal for int() KeyError, # dict without `int(bit)` key TypeError, # unsubscriptable object ): current_context = ( type(current) if isinstance(current, models.Model) else current ) raise ValueError( self.LOOKUP_ERROR_FMT.format( key=bit, context=current_context, accessor=self ) ) if callable(current): if safe and getattr(current, "alters_data", False): raise ValueError(self.ALTERS_DATA_ERROR_FMT.format(method=repr(current))) if not getattr(current, "do_not_call_in_templates", False): current = current() # important that we break in None case, or a relationship # spanning across a null-key will raise an exception in the # next iteration, instead of defaulting. if current is None: break return current except Exception: if not quiet: raise @property def bits(self): if self == "": return () return self.split(self.SEPARATOR) def get_field(self, model): """ Return the django model field for model in context, following relations. """ if not hasattr(model, "_meta"): return field = None for bit in self.bits: try: field = model._meta.get_field(bit) except FieldDoesNotExist: break if hasattr(field, "remote_field"): rel = getattr(field, "remote_field", None) model = getattr(rel, "model", model) return field def penultimate(self, context, quiet=True): """ Split the accessor on the right-most dot '.', return a tuple with: - the resolved left part. - the remainder Example:: >>> Accessor("a.b.c").penultimate({"a": {"a": 1, "b": {"c": 2, "d": 4}}}) ({"c": 2, "d": 4}, "c") """ path, _, remainder = self.rpartition(".") return A(path).resolve(context, quiet=quiet), remainder A = Accessor # alias class AttributeDict(OrderedDict): """ A wrapper around `collections.OrderedDict` that knows how to render itself as HTML style tag attributes. Any key with ``value is None`` will be skipped. The returned string is marked safe, so it can be used safely in a template. See `.as_html` for a usage example. """ blacklist = ("th", "td", "_ordering", "thead", "tbody", "tfoot") def _iteritems(self): for key, v in self.items(): value = v() if callable(v) else v if key not in self.blacklist and value is not None: yield (key, value) def as_html(self): """ Render to HTML tag attributes. Example: .. code-block:: python >>> from django_tables2.utils import AttributeDict >>> attrs = AttributeDict({'class': 'mytable', 'id': 'someid'}) >>> attrs.as_html() 'class="mytable" id="someid"' returns: `~django.utils.safestring.SafeUnicode` object """ return format_html_join(" ", '{}="{}"', self._iteritems()) def segment(sequence, aliases): """ Translates a flat sequence of items into a set of prefixed aliases. This allows the value set by `.QuerySet.order_by` to be translated into a list of columns that would have the same result. These are called "order by aliases" which are optionally prefixed column names:: >>> list(segment(('a', '-b', 'c'), ... {'x': ('a'), ... 'y': ('b', '-c'), ... 'z': ('-b', 'c')})) [('x', '-y'), ('x', 'z')] """ if not (sequence or aliases): return for alias, parts in aliases.items(): variants = { # alias: order by tuple alias: OrderByTuple(parts), OrderBy(alias).opposite: OrderByTuple(parts).opposite, } for valias, vparts in variants.items(): if list(sequence[: len(vparts)]) == list(vparts): tail_aliases = dict(aliases) del tail_aliases[alias] tail_sequence = sequence[len(vparts) :] if tail_sequence: for tail in segment(tail_sequence, tail_aliases): yield tuple(chain([valias], tail)) else: continue else: yield tuple([valias]) def signature(fn): """ Returns: tuple: Returns a (arguments, kwarg_name)-tuple: - the arguments (positional or keyword) - the name of the ** kwarg catch all. The self-argument for methods is always removed. """ signature = inspect.signature(fn) args = [] keywords = None for arg in signature.parameters.values(): if arg.kind == arg.VAR_KEYWORD: keywords = arg.name elif arg.kind == arg.VAR_POSITIONAL: continue # skip *args catch-all else: args.append(arg.name) return tuple(args), keywords def call_with_appropriate(fn, kwargs): """ Calls the function ``fn`` with the keyword arguments from ``kwargs`` it expects If the kwargs argument is defined, pass all arguments, else provide exactly the arguments wanted. If one of the arguments of ``fn`` are not contained in kwargs, ``fn`` will not be called and ``None`` will be returned. """ args, kwargs_name = signature(fn) # no catch-all defined, we need to exactly pass the arguments specified. if not kwargs_name: kwargs = {key: kwargs[key] for key in kwargs if key in args} # if any argument of fn is not in kwargs, just return None if any(arg not in kwargs for arg in args): return None return fn(**kwargs) def computed_values(d, kwargs=None): """ Returns a new `dict` that has callable values replaced with the return values. Example:: >>> compute_values({'foo': lambda: 'bar'}) {'foo': 'bar'} Arbitrarily deep structures are supported. The logic is as follows: 1. If the value is callable, call it and make that the new value. 2. If the value is an instance of dict, use ComputableDict to compute its keys. Example:: >>> def parents(): ... return { ... 'father': lambda: 'Foo', ... 'mother': 'Bar' ... } ... >>> a = { ... 'name': 'Brad', ... 'parents': parents ... } ... >>> computed_values(a) {'name': 'Brad', 'parents': {'father': 'Foo', 'mother': 'Bar'}} Arguments: d (dict): The original dictionary. kwargs: any extra keyword arguments will be passed to the callables, if the callable takes an argument with such a name. Returns: dict: with callable values replaced. """ kwargs = kwargs or {} result = {} for k, v in d.items(): if callable(v): v = call_with_appropriate(v, kwargs=kwargs) if isinstance(v, dict): v = computed_values(v, kwargs=kwargs) result[k] = v return result django-tables2-2.1.1/django_tables2/tables.py0000644000175000017500000006436513542204315020370 0ustar josephjosephimport copy from collections import OrderedDict from itertools import count from django.conf import settings from django.core.paginator import Paginator from django.db import models from django.template.loader import get_template from django.utils.encoding import force_str from . import columns from .config import RequestConfig from .data import TableData from .rows import BoundRows from .utils import Accessor, AttributeDict, OrderBy, OrderByTuple, Sequence class DeclarativeColumnsMetaclass(type): """ Metaclass that converts `.Column` objects defined on a class to the dictionary `.Table.base_columns`, taking into account parent class `base_columns` as well. """ def __new__(mcs, name, bases, attrs): attrs["_meta"] = opts = TableOptions(attrs.get("Meta", None), name) # extract declared columns cols, remainder = [], {} for attr_name, attr in attrs.items(): if isinstance(attr, columns.Column): attr._explicit = True cols.append((attr_name, attr)) else: remainder[attr_name] = attr attrs = remainder cols.sort(key=lambda x: x[1].creation_counter) # If this class is subclassing other tables, add their fields as # well. Note that we loop over the bases in *reverse* - this is # necessary to preserve the correct order of columns. parent_columns = [] for base in reversed(bases): if hasattr(base, "base_columns"): parent_columns = list(base.base_columns.items()) + parent_columns # Start with the parent columns base_columns = OrderedDict(parent_columns) # Possibly add some generated columns based on a model if opts.model: extra = OrderedDict() # honor Table.Meta.fields, fallback to model._meta.fields if opts.fields is not None: # Each item in opts.fields is the name of a model field or a # normal attribute on the model for field_name in opts.fields: field = Accessor(field_name).get_field(opts.model) extra[field_name] = columns.library.column_for_field(field) else: for field in opts.model._meta.fields: extra[field.name] = columns.library.column_for_field(field) # update base_columns with extra columns for key, col in extra.items(): # skip current col because the parent was explicitly defined, # and the current column is not. if key in base_columns and base_columns[key]._explicit is True: continue base_columns[key] = col # Explicit columns override both parent and generated columns base_columns.update(OrderedDict(cols)) # Apply any explicit exclude setting for exclusion in opts.exclude: if exclusion in base_columns: base_columns.pop(exclusion) # Remove any columns from our remainder, else columns from our parent class will remain for attr_name in remainder: if attr_name in base_columns: base_columns.pop(attr_name) # Set localize on columns for col_name in base_columns.keys(): localize_column = None if col_name in opts.localize: localize_column = True # unlocalize gets higher precedence if col_name in opts.unlocalize: localize_column = False if localize_column is not None: base_columns[col_name].localize = localize_column attrs["base_columns"] = base_columns return super().__new__(mcs, name, bases, attrs) class TableOptions: """ Extracts and exposes options for a `.Table` from a `.Table.Meta` when the table is defined. See `.Table` for documentation on the impact of variables in this class. Arguments: options (`.Table.Meta`): options for a table from `.Table.Meta` """ def __init__(self, options, class_name): super().__init__() self._check_types(options, class_name) DJANGO_TABLES2_TEMPLATE = getattr( settings, "DJANGO_TABLES2_TEMPLATE", "django_tables2/table.html" ) DJANGO_TABLES2_TABLE_ATTRS = getattr(settings, "DJANGO_TABLES2_TABLE_ATTRS", {}) self.attrs = getattr(options, "attrs", DJANGO_TABLES2_TABLE_ATTRS) self.row_attrs = getattr(options, "row_attrs", {}) self.pinned_row_attrs = getattr(options, "pinned_row_attrs", {}) self.default = getattr(options, "default", "—") self.empty_text = getattr(options, "empty_text", None) self.fields = getattr(options, "fields", None) self.exclude = getattr(options, "exclude", ()) order_by = getattr(options, "order_by", None) if isinstance(order_by, str): order_by = (order_by,) self.order_by = OrderByTuple(order_by) if order_by is not None else None self.order_by_field = getattr(options, "order_by_field", "sort") self.page_field = getattr(options, "page_field", "page") self.per_page = getattr(options, "per_page", 25) self.per_page_field = getattr(options, "per_page_field", "per_page") self.prefix = getattr(options, "prefix", "") self.show_header = getattr(options, "show_header", True) self.show_footer = getattr(options, "show_footer", True) self.sequence = getattr(options, "sequence", ()) self.orderable = getattr(options, "orderable", True) self.model = getattr(options, "model", None) self.template_name = getattr(options, "template_name", DJANGO_TABLES2_TEMPLATE) self.localize = getattr(options, "localize", ()) self.unlocalize = getattr(options, "unlocalize", ()) def _check_types(self, options, class_name): """ Check class Meta attributes to prevent common mistakes. """ if options is None: return checks = { (bool,): ["show_header", "show_footer", "orderable"], (int,): ["per_page"], (tuple, list, set): ["fields", "sequence", "exclude", "localize", "unlocalize"], str: ["template_name", "prefix", "order_by_field", "page_field", "per_page_field"], (dict,): ["attrs", "row_attrs", "pinned_row_attrs"], (tuple, list, str): ["order_by"], (type(models.Model),): ["model"], } for types, keys in checks.items(): for key in keys: value = getattr(options, key, None) if value is not None and not isinstance(value, types): expression = "{}.{} = {}".format(class_name, key, value.__repr__()) raise TypeError( "{} (type {}), but type must be one of ({})".format( expression, type(value).__name__, ", ".join([t.__name__ for t in types]) ) ) class TableBase: """ A representation of a table. Arguments: data (QuerySet, list of dicts): The data to display. This is a required variable, a `TypeError` will be raised if it's not passed. order_by: (tuple or str): The default ordering tuple or comma separated str. A hyphen `-` can be used to prefix a column name to indicate *descending* order, for example: `("name", "-age")` or `name,-age`. orderable (bool): Enable/disable column ordering on this table empty_text (str): Empty text to render when the table has no data. (default `.Table.Meta.empty_text`) exclude (iterable or str): The names of columns that should not be included in the table. attrs (dict): HTML attributes to add to the ``
...
`` tag. When accessing the attribute, the value is always returned as an `.AttributeDict` to allow easily conversion to HTML. row_attrs (dict): Add custom html attributes to the table rows. Allows custom HTML attributes to be specified which will be added to the ```` tag of the rendered table. pinned_row_attrs (dict): Same as row_attrs but for pinned rows. sequence (iterable): The sequence/order of columns the columns (from left to right). Items in the sequence must be :term:`column names `, or `"..."` (string containing three periods). `'...'` can be used as a catch-all for columns that are not specified. prefix (str): A prefix for query string fields. To avoid name-clashes when using multiple tables on single page. order_by_field (str): If not `None`, defines the name of the *order by* query string field in the URL. page_field (str): If not `None`, defines the name of the *current page* query string field. per_page_field (str): If not `None`, defines the name of the *per page* query string field. template_name (str): The template to render when using ``{% render_table %}`` (defaults to DJANGO_TABLES2_TEMPLATE, which is ``"django_tables2/table.html"`` by default). default (str): Text to render in empty cells (determined by `.Column.empty_values`, default `.Table.Meta.default`) request: Django's request to avoid using `RequestConfig` show_header (bool): If `False`, the table will not have a header (``), defaults to `True` show_footer (bool): If `False`, the table footer will not be rendered, even if some columns have a footer, defaults to `True`. extra_columns (str, `.Column`): list of `(name, column)`-tuples containing extra columns to add to the instance. If `column` is `None`, the column with `name` will be removed from the table. """ def __init__( self, data=None, order_by=None, orderable=None, empty_text=None, exclude=None, attrs=None, row_attrs=None, pinned_row_attrs=None, sequence=None, prefix=None, order_by_field=None, page_field=None, per_page_field=None, template_name=None, default=None, request=None, show_header=None, show_footer=True, extra_columns=None, ): super().__init__() # note that although data is a keyword argument, it used to be positional # so it is assumed to be the first argument to this method. if data is None: raise TypeError("Argument data to {} is required".format(type(self).__name__)) self.exclude = exclude or self._meta.exclude self.sequence = sequence self.data = TableData.from_data(data=data) self.data.set_table(self) if default is None: default = self._meta.default self.default = default # Pinned rows #406 self.pinned_row_attrs = AttributeDict(pinned_row_attrs or self._meta.pinned_row_attrs) self.pinned_data = { "top": self.get_top_pinned_data(), "bottom": self.get_bottom_pinned_data(), } self.rows = BoundRows(data=self.data, table=self, pinned_data=self.pinned_data) self.attrs = AttributeDict(attrs if attrs is not None else self._meta.attrs) for tag in ["thead", "tbody", "tfoot"]: # Add these attrs even if they haven't been passed so we can safely refer to them in the templates self.attrs[tag] = AttributeDict(self.attrs.get(tag, {})) self.row_attrs = AttributeDict(row_attrs or self._meta.row_attrs) self.empty_text = empty_text if empty_text is not None else self._meta.empty_text self.orderable = orderable self.prefix = prefix self.order_by_field = order_by_field self.page_field = page_field self.per_page_field = per_page_field self.show_header = show_header self.show_footer = show_footer # Make a copy so that modifying this will not touch the class # definition. Note that this is different from forms, where the # copy is made available in a ``fields`` attribute. base_columns = copy.deepcopy(type(self).base_columns) if extra_columns is not None: for name, column in extra_columns: if column is None and name in base_columns: del base_columns[name] else: base_columns[name] = column # Keep fully expanded ``sequence`` at _sequence so it's easily accessible # during render. The priority is as follows: # 1. sequence passed in as an argument # 2. sequence declared in ``Meta`` # 3. sequence defaults to '...' if sequence is not None: sequence = sequence elif self._meta.sequence: sequence = self._meta.sequence else: if self._meta.fields is not None: sequence = tuple(self._meta.fields) + ("...",) else: sequence = ("...",) sequence = Sequence(sequence) self._sequence = sequence.expand(base_columns.keys()) # reorder columns based on sequence. base_columns = OrderedDict(((x, base_columns[x]) for x in sequence if x in base_columns)) self.columns = columns.BoundColumns(self, base_columns) # `None` value for order_by means no order is specified. This means we # `shouldn't touch our data's ordering in any way. *However* # `table.order_by = None` means "remove any ordering from the data" # (it's equivalent to `table.order_by = ()`). if order_by is None and self._meta.order_by is not None: order_by = self._meta.order_by if order_by is None: self._order_by = None # If possible inspect the ordering on the data we were given and # update the table to reflect that. order_by = self.data.ordering if order_by is not None: self.order_by = order_by else: self.order_by = order_by self.template_name = template_name # If a request is passed, configure for request if request: RequestConfig(request).configure(self) self._counter = count() def get_top_pinned_data(self): """ Return data for top pinned rows containing data for each row. Iterable type like: QuerySet, list of dicts, list of objects. Having a non-zero number of pinned rows will not result in an empty result set message being rendered, even if there are no regular data rows Returns: `None` (default) no pinned rows at the top, iterable, data for pinned rows at the top. Note: To show pinned row this method should be overridden. Example: >>> class TableWithTopPinnedRows(Table): ... def get_top_pinned_data(self): ... return [{ ... "column_a" : "some value", ... "column_c" : "other value", ... }] """ return None def get_bottom_pinned_data(self): """ Return data for bottom pinned rows containing data for each row. Iterable type like: QuerySet, list of dicts, list of objects. Having a non-zero number of pinned rows will not result in an empty result set message being rendered, even if there are no regular data rows Returns: `None` (default) no pinned rows at the bottom, iterable, data for pinned rows at the bottom. Note: To show pinned row this method should be overridden. Example: >>> class TableWithBottomPinnedRows(Table): ... def get_bottom_pinned_data(self): ... return [{ ... "column_a" : "some value", ... "column_c" : "other value", ... }] """ return None def before_render(self, request): """ A way to hook into the moment just before rendering the template. Can be used to hide a column. Arguments: request: contains the `WGSIRequest` instance, containing a `user` attribute if `.django.contrib.auth.middleware.AuthenticationMiddleware` is added to your `MIDDLEWARE_CLASSES`. Example:: class Table(tables.Table): name = tables.Column(orderable=False) country = tables.Column(orderable=False) def before_render(self, request): if request.user.has_perm('foo.delete_bar'): self.columns.hide('country') else: self.columns.show('country') """ return def as_html(self, request): """ Render the table to an HTML table, adding `request` to the context. """ # reset counter for new rendering self._counter = count() template = get_template(self.template_name) context = {"table": self, "request": request} self.before_render(request) return template.render(context) def as_values(self, exclude_columns=None): """ Return a row iterator of the data which would be shown in the table where the first row is the table headers. arguments: exclude_columns (iterable): columns to exclude in the data iterator. This can be used to output the table data as CSV, excel, for example using the `~.export.ExportMixin`. If a column is defined using a :ref:`table.render_FOO`, the returned value from that method is used. If you want to differentiate between the rendered cell and a value, use a `value_Foo`-method:: class Table(tables.Table): name = tables.Column() def render_name(self, value): return format_html('{}', value) def value_name(self, value): return value will have a value wrapped in `` in the rendered HTML, and just returns the value when `as_values()` is called. Note that any invisible columns will be part of the row iterator. """ if exclude_columns is None: exclude_columns = () columns = [ column for column in self.columns.iterall() if not (column.column.exclude_from_export or column.name in exclude_columns) ] yield [force_str(column.header, strings_only=True) for column in columns] for row in self.rows: yield [ force_str(row.get_cell_value(column.name), strings_only=True) for column in columns ] def has_footer(self): """ Returns True if any of the columns define a ``_footer`` attribute or a ``render_footer()`` method """ return self.show_footer and any(column.has_footer() for column in self.columns) @property def show_header(self): return self._show_header if self._show_header is not None else self._meta.show_header @show_header.setter def show_header(self, value): self._show_header = value @property def order_by(self): return self._order_by @order_by.setter def order_by(self, value): """ Order the rows of the table based on columns. Arguments: value: iterable or comma separated string of order by aliases. """ # collapse empty values to () order_by = () if not value else value # accept string order_by = order_by.split(",") if isinstance(order_by, str) else order_by valid = [] # everything's been converted to a iterable, accept iterable! for alias in order_by: name = OrderBy(alias).bare if name in self.columns and self.columns[name].orderable: valid.append(alias) self._order_by = OrderByTuple(valid) self.data.order_by(self._order_by) @property def order_by_field(self): return ( self._order_by_field if self._order_by_field is not None else self._meta.order_by_field ) @order_by_field.setter def order_by_field(self, value): self._order_by_field = value @property def page_field(self): return self._page_field if self._page_field is not None else self._meta.page_field @page_field.setter def page_field(self, value): self._page_field = value def paginate(self, paginator_class=Paginator, per_page=None, page=1, *args, **kwargs): """ Paginates the table using a paginator and creates a ``page`` property containing information for the current page. Arguments: paginator_class (`~django.core.paginator.Paginator`): A paginator class to paginate the results. per_page (int): Number of records to display on each page. page (int): Page to display. Extra arguments are passed to the paginator. Pagination exceptions (`~django.core.paginator.EmptyPage` and `~django.core.paginator.PageNotAnInteger`) may be raised from this method and should be handled by the caller. """ per_page = per_page or self._meta.per_page self.paginator = paginator_class(self.rows, per_page, *args, **kwargs) self.page = self.paginator.page(page) return self @property def per_page_field(self): return ( self._per_page_field if self._per_page_field is not None else self._meta.per_page_field ) @per_page_field.setter def per_page_field(self, value): self._per_page_field = value @property def prefix(self): return self._prefix if self._prefix is not None else self._meta.prefix @prefix.setter def prefix(self, value): self._prefix = value @property def prefixed_order_by_field(self): return "%s%s" % (self.prefix, self.order_by_field) @property def prefixed_page_field(self): return "%s%s" % (self.prefix, self.page_field) @property def prefixed_per_page_field(self): return "%s%s" % (self.prefix, self.per_page_field) @property def sequence(self): return self._sequence @sequence.setter def sequence(self, value): if value: value = Sequence(value) value.expand(self.base_columns.keys()) self._sequence = value @property def orderable(self): if self._orderable is not None: return self._orderable else: return self._meta.orderable @orderable.setter def orderable(self, value): self._orderable = value @property def template_name(self): if self._template is not None: return self._template else: return self._meta.template_name @template_name.setter def template_name(self, value): self._template = value @property def paginated_rows(self): """ Return the rows for the current page if the table is paginated, else all rows. """ if hasattr(self, "page"): return self.page.object_list return self.rows def get_column_class_names(self, classes_set, bound_column): """ Returns a set of HTML class names for cells (both ``td`` and ``th``) of a **bound column** in this table. By default this returns the column class names defined in the table's attributes. This method can be overridden to change the default behavior, for example to simply `return classes_set`. Arguments: classes_set(set of string): a set of class names to be added to the cell, retrieved from the column's attributes. In the case of a header cell (th), this also includes ordering classes. To set the classes for a column, see `.Column`. To configure ordering classes, see :ref:`ordering-class-name` bound_column(`.BoundColumn`): the bound column the class names are determined for. Useful for accessing `bound_column.name`. Returns: A set of class names to be added to cells of this column If you want to add the column names to the list of classes for a column, override this method in your custom table:: class MyTable(tables.Table): ... def get_column_class_names(self, classes_set, bound_column): classes_set = super().get_column_class_names(classes_set, bound_column) classes_set.add(bound_column.name) return classes_set """ return classes_set Table = DeclarativeColumnsMetaclass("Table", (TableBase,), {}) def table_factory(model, table=Table, fields=None, exclude=None, localize=None): """ Return Table class for given `model`, equivalent to defining a custom table class:: class MyTable(tables.Table): class Meta: model = model Arguments: model (`~django.db.models.Model`): Model associated with the new table table (`.Table`): Base Table class used to create the new one fields (list of str): Fields displayed in tables exclude (list of str): Fields exclude in tables localize (list of str): Fields to localize """ attrs = {"model": model} if fields is not None: attrs["fields"] = fields if exclude is not None: attrs["exclude"] = exclude if localize is not None: attrs["localize"] = localize # If parent form class already has an inner Meta, the Meta we're # creating needs to inherit from the parent's inner meta. parent = (table.Meta, object) if hasattr(table, "Meta") else (object,) Meta = type("Meta", parent, attrs) # Give this new table class a reasonable name. class_name = model.__name__ + "AutogeneratedTable" # Class attributes for the new table class. table_class_attrs = {"Meta": Meta} return type(table)(class_name, (table,), table_class_attrs) django-tables2-2.1.1/django_tables2/__init__.py0000644000175000017500000000171613542204315020644 0ustar josephjosephfrom .columns import ( BooleanColumn, CheckBoxColumn, Column, DateColumn, DateTimeColumn, EmailColumn, FileColumn, JSONColumn, LinkColumn, ManyToManyColumn, RelatedLinkColumn, TemplateColumn, TimeColumn, URLColumn, ) from .config import RequestConfig from .paginators import LazyPaginator from .tables import Table, TableBase, table_factory from .utils import A from .views import MultiTableMixin, SingleTableMixin, SingleTableView __version__ = "2.1.1" __all__ = ( "Table", "TableBase", "table_factory", "BooleanColumn", "Column", "CheckBoxColumn", "DateColumn", "DateTimeColumn", "EmailColumn", "FileColumn", "JSONColumn", "LinkColumn", "ManyToManyColumn", "RelatedLinkColumn", "TemplateColumn", "TimeColumn", "URLColumn", "RequestConfig", "A", "SingleTableMixin", "SingleTableView", "MultiTableMixin", "LazyPaginator", ) django-tables2-2.1.1/django_tables2/columns/0000755000175000017500000000000013542204315020206 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/columns/jsoncolumn.py0000644000175000017500000000361313542204315022752 0ustar josephjosephimport json from django.utils.html import format_html from django.utils.text import capfirst from ..utils import AttributeDict from .base import library from .linkcolumn import BaseLinkColumn try: from django.contrib.postgres.fields import HStoreField, JSONField POSTGRES_AVAILABLE = True except ImportError: # psycopg2 is not available, cannot import from django.contrib.postgres. # JSONColumn might still be useful to add manually. POSTGRES_AVAILABLE = False @library.register class JSONColumn(BaseLinkColumn): """ Render the contents of `~django.contrib.postgres.fields.JSONField` or `~django.contrib.postgres.fields.HStoreField` as an indented string. .. versionadded :: 1.5.0 .. note:: Automatic rendering of data to this column requires PostgreSQL support (psycopg2 installed) to import the fields, but this column can also be used manually without it. Arguments: json_dumps_kwargs: kwargs passed to `json.dumps`, defaults to `{'indent': 2}` attrs (dict): In addition to *attrs* keys supported by `~.Column`, the following are available: - ``pre`` -- ``
`` around the rendered JSON string in ``
`` elements. """ def __init__(self, json_dumps_kwargs=None, **kwargs): self.json_dumps_kwargs = ( json_dumps_kwargs if json_dumps_kwargs is not None else {"indent": 2} ) super().__init__(**kwargs) def render(self, record, value): return format_html( "
{}
", AttributeDict(self.attrs.get("pre", {})).as_html(), json.dumps(value, **self.json_dumps_kwargs), ) @classmethod def from_field(cls, field): if POSTGRES_AVAILABLE: if isinstance(field, JSONField) or isinstance(field, HStoreField): return cls(verbose_name=capfirst(field.verbose_name)) django-tables2-2.1.1/django_tables2/columns/linkcolumn.py0000644000175000017500000001374613542204315022746 0ustar josephjosephfrom .base import Column, library class BaseLinkColumn(Column): """ The base for other columns that render links. Arguments: text (str or callable): If set, this value will be used to render the text inside link instead of value. The callable gets the record being rendered as argument. attrs (dict): In addition to ``attrs`` keys supported by `~.Column`, the following are available: - `a` -- ```` in ``
`` elements. """ def __init__(self, text=None, *args, **kwargs): super().__init__(*args, **kwargs) self.text = text def text_value(self, record, value): if self.text is None: return value return self.text(record) if callable(self.text) else self.text def value(self, record, value): """ Returns the content for a specific cell similarly to `.render` however without any html content. """ return self.text_value(record, value) def render(self, record, value): return self.text_value(record, value) @library.register class LinkColumn(BaseLinkColumn): """ Renders a normal value as an internal hyperlink to another page. .. note :: This column should not be used anymore, the `linkify` keyword argument to regular columns can be used to achieve the same results. It's common to have the primary value in a row hyperlinked to the page dedicated to that record. The first arguments are identical to that of `~django.urls.reverse` and allows an internal URL to be described. If this argument is `None`, then `get_absolute_url`. (see Django references) will be used. The last argument *attrs* allows custom HTML attributes to be added to the rendered ```` tag. Arguments: viewname (str or None): See `~django.urls.reverse`, or use `None` to use the model's `get_absolute_url` urlconf (str): See `~django.urls.reverse`. args (list): See `~django.urls.reverse`. [2]_ kwargs (dict): See `~django.urls.reverse`. [2]_ current_app (str): See `~django.urls.reverse`. attrs (dict): HTML attributes that are added to the rendered ``...`` tag. text (str or callable): Either static text, or a callable. If set, this will be used to render the text inside link instead of value (default). The callable gets the record being rendered as argument. .. [2] In order to create a link to a URL that relies on information in the current row, `.Accessor` objects can be used in the *args* or *kwargs* arguments. The accessor will be resolved using the row's record before `~django.urls.reverse` is called. Example: .. code-block:: python # models.py class Person(models.Model): name = models.CharField(max_length=200) # urls.py urlpatterns = patterns('', url("people/([0-9]+)/", views.people_detail, name="people_detail") ) # tables.py from django_tables2.utils import A # alias for Accessor class PeopleTable(tables.Table): name = tables.LinkColumn("people_detail", args=[A("pk")]) In order to override the text value (i.e. ``text``) consider the following example: .. code-block:: python # tables.py from django_tables2.utils import A # alias for Accessor class PeopleTable(tables.Table): name = tables.LinkColumn("people_detail", text="static text", args=[A("pk")]) age = tables.LinkColumn("people_detail", text=lambda record: record.name, args=[A("pk")]) In the first example, a static text would be rendered (``"static text"``) In the second example, you can specify a callable which accepts a record object (and thus can return anything from it) In addition to *attrs* keys supported by `.Column`, the following are available: - `a` -- ```` elements in ````. Adding attributes to the ````-tag looks like this:: class PeopleTable(tables.Table): first_name = tables.LinkColumn(attrs={ "a": {"style": "color: red;"} }) """ def __init__( self, viewname=None, urlconf=None, args=None, kwargs=None, current_app=None, attrs=None, **extra ): super().__init__( attrs=attrs, linkify=dict( viewname=viewname, urlconf=urlconf, args=args, kwargs=kwargs, current_app=current_app, ), **extra ) @library.register class RelatedLinkColumn(LinkColumn): """ Render a link to a related object using related object's ``get_absolute_url``, same parameters as ``~.LinkColumn``. .. note :: This column should not be used anymore, the `linkify` keyword argument to regular columns can be used achieve the same results. If the related object does not have a method called ``get_absolute_url``, or if it is not callable, the link will be rendered as '#'. Traversing relations is also supported, suppose a Person has a foreign key to Country which in turn has a foreign key to Continent:: class PersonTable(tables.Table): name = tables.Column() country = tables.RelatedLinkColumn() continent = tables.RelatedLinkColumn(accessor="country.continent") will render: - in column 'country', link to ``person.country.get_absolute_url()`` with the output of ``str(person.country)`` as ```` contents. - in column 'continent', a link to ``person.country.continent.get_absolute_url()`` with the output of ``str(person.country.continent)`` as ```` contents. Alternative contents of ```` can be supplied using the ``text`` keyword argument as documented for `~.columns.LinkColumn`. """ django-tables2-2.1.1/django_tables2/columns/filecolumn.py0000644000175000017500000000570213542204315022721 0ustar josephjosephimport os from django.db import models from django.utils.html import format_html from django.utils.text import capfirst from ..utils import AttributeDict from .base import library from .linkcolumn import BaseLinkColumn @library.register class FileColumn(BaseLinkColumn): """ Attempts to render `.FieldFile` (or other storage backend `.File`) as a hyperlink. When the file is accessible via a URL, the file is rendered as a hyperlink. The `.basename` is used as the text, wrapped in a span:: receipt.pdf When unable to determine the URL, a ``span`` is used instead:: receipt.pdf `.Column.attrs` keys ``a`` and ``span`` can be used to add additional attributes. Arguments: verify_exists (bool): attempt to determine if the file exists If *verify_exists*, the HTML class ``exists`` or ``missing`` is added to the element to indicate the integrity of the storage. text (str or callable): Either static text, or a callable. If set, this will be used to render the text inside the link instead of the file's ``basename`` (default) """ def __init__(self, verify_exists=True, **kwargs): self.verify_exists = verify_exists super().__init__(**kwargs) def get_url(self, value, record): storage = getattr(value, "storage", None) if not storage: return None return storage.url(value.name) def text_value(self, record, value): if self.text is None: return os.path.basename(value.name) return super().text_value(record, value) def render(self, record, value): attrs = AttributeDict(self.attrs.get("span", {})) classes = [c for c in attrs.get("class", "").split(" ") if c] exists = None storage = getattr(value, "storage", None) if storage: # we'll assume value is a `django.db.models.fields.files.FieldFile` if self.verify_exists: exists = storage.exists(value.name) else: if self.verify_exists and hasattr(value, "name"): # ignore negatives, perhaps the file has a name but it doesn't # represent a local path... better to stay neutral than give a # false negative. exists = os.path.exists(value.name) or exists if exists is not None: classes.append("exists" if exists else "missing") attrs["title"] = value.name attrs["class"] = " ".join(classes) return format_html( "{text}", attrs=attrs.as_html(), text=self.text_value(record, value), ) @classmethod def from_field(cls, field): if isinstance(field, models.FileField): return cls(verbose_name=capfirst(field.verbose_name)) django-tables2-2.1.1/django_tables2/columns/checkboxcolumn.py0000644000175000017500000000603313542204315023566 0ustar josephjosephfrom django.utils.safestring import mark_safe from django_tables2.utils import Accessor, AttributeDict from .base import Column, library @library.register class CheckBoxColumn(Column): """ A subclass of `.Column` that renders as a checkbox form input. This column allows a user to *select* a set of rows. The selection information can then be used to apply some operation (e.g. "delete") onto the set of objects that correspond to the selected rows. The value that is extracted from the :term:`table data` for this column is used as the value for the checkbox, i.e. ```` This class implements some sensible defaults: - HTML input's ``name`` attribute is the :term:`column name` (can override via *attrs* argument). - ``orderable`` defaults to `False`. Arguments: attrs (dict): In addition to *attrs* keys supported by `~.Column`, the following are available: - ``input`` -- ```` elements in both ```` and ````. - ``th__input`` -- Replaces ``input`` attrs in header cells. - ``td__input`` -- Replaces ``input`` attrs in body cells. checked (`~.Accessor`, bool, callable): Allow rendering the checkbox as checked. If it resolves to a truthy value, the checkbox will be rendered as checked. .. note:: You might expect that you could select multiple checkboxes in the rendered table and then *do something* with that. This functionality is not implemented. If you want something to actually happen, you will need to implement that yourself. """ def __init__(self, attrs=None, checked=None, **extra): self.checked = checked kwargs = {"orderable": False, "attrs": attrs} kwargs.update(extra) super().__init__(**kwargs) @property def header(self): default = {"type": "checkbox"} general = self.attrs.get("input") specific = self.attrs.get("th__input") attrs = AttributeDict(default, **(specific or general or {})) return mark_safe("" % attrs.as_html()) def render(self, value, bound_column, record): default = {"type": "checkbox", "name": bound_column.name, "value": value} if self.is_checked(value, record): default.update({"checked": "checked"}) general = self.attrs.get("input") specific = self.attrs.get("td__input") attrs = AttributeDict(default, **(specific or general or {})) return mark_safe("" % attrs.as_html()) def is_checked(self, value, record): """ Determine if the checkbox should be checked """ if self.checked is None: return False if self.checked is True: return True if callable(self.checked): return bool(self.checked(value, record)) checked = Accessor(self.checked) if checked in record: return bool(record[checked]) return False django-tables2-2.1.1/django_tables2/columns/manytomanycolumn.py0000644000175000017500000000736013542204315024200 0ustar josephjosephfrom django.db import models from django.utils.encoding import force_str from django.utils.html import conditional_escape, mark_safe from django.utils.text import capfirst from .base import Column, LinkTransform, library @library.register class ManyToManyColumn(Column): """ Display the list of objects from a `ManyRelatedManager` Ordering is disabled for this column. Arguments: transform: callable to transform each item to text, it gets an item as argument and must return a string-like representation of the item. By default, it calls `~django.utils.force_str` on each item. filter: callable to filter, limit or order the QuerySet, it gets the `ManyRelatedManager` as first argument and must return a filtered QuerySet. By default, it returns `all()` separator: separator string to join the items with. default: ``", "`` linkify_item: callable, arguments to reverse() or `True` to wrap items in a ```` tag. For a detailed explanation, see ``linkify`` argument to ``Column``. For example, when displaying a list of friends with their full name:: # models.py class Person(models.Model): first_name = models.CharField(max_length=200) last_name = models.CharField(max_length=200) friends = models.ManyToManyField(Person) is_active = models.BooleanField(default=True) @property def name(self): return '{} {}'.format(self.first_name, self.last_name) # tables.py class PersonTable(tables.Table): name = tables.Column(order_by=("last_name", "first_name")) friends = tables.ManyToManyColumn(transform=lambda user: u.name) If only the active friends should be displayed, you can use the `filter` argument:: friends = tables.ManyToManyColumn(filter=lambda qs: qs.filter(is_active=True)) """ def __init__( self, transform=None, filter=None, separator=", ", linkify_item=None, *args, **kwargs ): kwargs.setdefault("orderable", False) super().__init__(*args, **kwargs) if transform is not None: self.transform = transform if filter is not None: self.filter = filter self.separator = separator link_kwargs = None if callable(linkify_item): link_kwargs = dict(url=linkify_item) elif isinstance(linkify_item, (dict, tuple)): link_kwargs = dict(reverse_args=linkify_item) elif linkify_item is True: link_kwargs = dict() if link_kwargs is not None: self.linkify_item = LinkTransform(attrs=self.attrs.get("a", {}), **link_kwargs) def transform(self, obj): """ Transform is applied to each item of the list of objects from the ManyToMany relation. """ return force_str(obj) def filter(self, qs): """ Filter is called on the ManyRelatedManager to allow ordering, filtering or limiting on the set of related objects. """ return qs.all() def render(self, value): # if value is None or not value.exists(): if not value.exists(): return self.default items = [] for item in self.filter(value): content = conditional_escape(self.transform(item)) if hasattr(self, "linkify_item"): content = self.linkify_item(content=content, record=item) items.append(content) return mark_safe(conditional_escape(self.separator).join(items)) @classmethod def from_field(cls, field): if isinstance(field, models.ManyToManyField): return cls(verbose_name=capfirst(field.verbose_name)) django-tables2-2.1.1/django_tables2/columns/timecolumn.py0000644000175000017500000000161213542204315022734 0ustar josephjosephfrom django.db import models from django.utils.text import capfirst from .base import library from .templatecolumn import TemplateColumn @library.register class TimeColumn(TemplateColumn): """ A column that renders times in the local timezone. Arguments: format (str): format string in same format as Django's ``time`` template filter (optional) short (bool): if *format* is not specified, use Django's ``TIME_FORMAT`` setting """ def __init__(self, format=None, *args, **kwargs): if format is None: format = "TIME_FORMAT" template = '{{ value|date:"%s"|default:default }}' % format super().__init__(template_code=template, *args, **kwargs) @classmethod def from_field(cls, field): if isinstance(field, models.TimeField): return cls(verbose_name=capfirst(field.verbose_name)) django-tables2-2.1.1/django_tables2/columns/base.py0000644000175000017500000007714313542204315021506 0ustar josephjosephfrom collections import OrderedDict from itertools import islice from django.core.exceptions import ImproperlyConfigured from django.urls import reverse from django.utils.html import format_html from django.utils.safestring import SafeData from django.utils.text import capfirst from ..utils import ( Accessor, AttributeDict, OrderBy, OrderByTuple, call_with_appropriate, computed_values, ) class Library: """A collection of columns.""" def __init__(self): self.columns = [] def register(self, column): if not hasattr(column, "from_field"): raise ImproperlyConfigured( "{} is not a subclass of Column".format(column.__class__.__name__) ) self.columns.append(column) return column def column_for_field(self, field): """ Return a column object suitable for model field. Returns: `.Column` object or `None` """ if field is None: return self.columns[0]() # iterate in reverse order as columns are registered in order # of least to most specialised (i.e. Column is registered # first). This also allows user-registered columns to be # favoured. for candidate in reversed(self.columns): column = candidate.from_field(field) if column is None: continue return column # The library is a mechanism for announcing what columns are available. Its # current use is to allow the table metaclass to ask columns if they're a # suitable match for a model field, and if so to return an approach instance. library = Library() class LinkTransform: """Object used to generate attributes for the ``-tag to wrap the cell content in.""" viewname = None accessor = None attrs = None def __init__(self, url=None, accessor=None, attrs=None, reverse_args=None): """ arguments: url (callable): If supplied, the result of this callable will be used as ``href`` attribute. accessor (Accessor): if supplied, the accessor will be used to decide on which object ``get_absolute_url()`` is called. attrs (dict): Customize attributes for the ```` tag. reverse_args (dict, tuple): Arguments to ``django.urls.reverse()``. If dict, the arguments are assumed to be keyword arguments to ``reverse()``, if tuple, a ``(viewname, args)`` or ``(viewname, kwargs)`` """ self.url = url self.attrs = attrs self.accessor = accessor if isinstance(reverse_args, (list, tuple)): viewname, args = reverse_args reverse_args = {"viewname": viewname} reverse_args["kwargs" if isinstance(args, dict) else "args"] = args self.reverse_args = reverse_args or {} def compose_url(self, **kwargs): if self.url and callable(self.url): return call_with_appropriate(self.url, kwargs) bound_column = kwargs.get("bound_column", None) record = kwargs["record"] if self.reverse_args.get("viewname", None) is not None: return self.call_reverse(record=record) if bound_column is None and self.accessor is None: accessor = Accessor("") else: accessor = Accessor(self.accessor if self.accessor is not None else bound_column.name) context = accessor.resolve(record) if not hasattr(context, "get_absolute_url"): if hasattr(record, "get_absolute_url"): context = record else: raise TypeError( "for linkify=True, '{}' must have a method get_absolute_url".format( str(context) ) ) return context.get_absolute_url() def call_reverse(self, record): """ Prepares the arguments to reverse() for this record and calls reverse() """ def resolve_if_accessor(val): return val.resolve(record) if isinstance(val, Accessor) else val params = self.reverse_args.copy() params["viewname"] = resolve_if_accessor(params["viewname"]) if params.get("urlconf", None): params["urlconf"] = resolve_if_accessor(params["urlconf"]) if params.get("args", None): params["args"] = [resolve_if_accessor(a) for a in params["args"]] if params.get("kwargs", None): params["kwargs"] = { key: resolve_if_accessor(val) for key, val in params["kwargs"].items() } if params.get("current_app", None): params["current_app"] = resolve_if_accessor(params["current_app"]) return reverse(**params) def get_attrs(self, **kwargs): attrs = AttributeDict(self.attrs or {}) attrs["href"] = self.compose_url(**kwargs) return attrs def __call__(self, content, **kwargs): attrs = self.get_attrs(**kwargs) if attrs["href"] is None: return content return format_html("{}", attrs.as_html(), content) @library.register class Column: """ Represents a single column of a table. `.Column` objects control the way a column (including the cells that fall within it) are rendered. Arguments: attrs (dict): HTML attributes for elements that make up the column. This API is extended by subclasses to allow arbitrary HTML attributes to be added to the output. By default `.Column` supports: - ``th`` -- ``table/thead/tr/th`` elements - ``td`` -- ``table/tbody/tr/td`` elements - ``cell`` -- fallback if ``th`` or ``td`` is not defined - ``a`` -- To control the attributes for the ``a`` tag if the cell is wrapped in a link. accessor (str or `~.Accessor`): An accessor that describes how to extract values for this column from the :term:`table data`. default (str or callable): The default value for the column. This can be a value or a callable object [1]_. If an object in the data provides `None` for a column, the default will be used instead. The default value may affect ordering, depending on the type of data the table is using. The only case where ordering is not affected is when a `.QuerySet` is used as the table data (since sorting is performed by the database). empty_values (iterable): list of values considered as a missing value, for which the column will render the default value. Defaults to `(None, '')` exclude_from_export (bool): If `True`, this column will not be added to the data iterator returned from as_values(). footer (str, callable): Defines the footer of this column. If a callable is passed, it can take optional keyword arguments `column`, `bound_column` and `table`. order_by (str, tuple or `.Accessor`): Allows one or more accessors to be used for ordering rather than *accessor*. orderable (bool): If `False`, this column will not be allowed to influence row ordering/sorting. verbose_name (str): A human readable version of the column name. visible (bool): If `True`, this column will be rendered. Columns with `visible=False` will not be rendered, but will be included in ``.Table.as_values()`` and thus also in :ref:`export`. localize: If the cells in this column will be localized by the `localize` filter: - If `True`, force localization - If `False`, values are not localized - If `None` (default), localization depends on the ``USE_L10N`` setting. linkify (bool, str, callable, dict, tuple): Controls if cell content will be wrapped in an ``a`` tag. The different ways to define the ``href`` attribute: - If `True`, the ``record.get_absolute_url()`` or the related model's `get_absolute_url()` is used. - If a callable is passed, the returned value is used, if it's not ``None``. - If a `dict` is passed, it's passed on to ``~django.urls.reverse``. - If a `tuple` is passed, it must be either a (viewname, args) or (viewname, kwargs) tuple, which is also passed to ``~django.urls.reverse``. initial_sort_descending (bool): If `True`, a column will sort in descending order on "first click" after table has been rendered. If `False`, column will follow default behavior, and sort ascending on "first click". Defaults to `False`. Examples, assuming this model:: class Blog(models.Model): title = models.CharField(max_length=100) body = model.TextField() user = model.ForeignKey(get_user_model(), on_delete=models.CASCADE) Using the ``linkify`` argument to control the linkification. These columns will all display the value returned from `str(record.user)`:: # If the column is named 'user', the column will use record.user.get_absolute_url() user = tables.Column(linkify=True) # We can also do that explicitly: user = tables.Column(linkify=lambda record: record.user.get_absolute_url()) # or, if no get_absolute_url is defined, or a custom link is required, we have a couple # of ways to define what is passed to reverse() user = tables.Column(linkify={"viewname": "user_detail", "args":(tables.A("user.pk"),)}) user = tables.Column(linkify=("user_detail", (tables.A("user.pk"), ))) # (viewname, args) user = tables.Column(linkify=("user_detail", {"pk": tables.A("user.pk")})) # (viewname, kwargs) .. [1] The provided callable object must not expect to receive any arguments. """ # Tracks each time a Column instance is created. Used to retain order. creation_counter = 0 empty_values = (None, "") # by default, contents are not wrapped in an -tag. link = None # Explicit is set to True if the column is defined as an attribute of a # class, used to give explicit columns precedence. _explicit = False def __init__( self, verbose_name=None, accessor=None, default=None, visible=True, orderable=None, attrs=None, order_by=None, empty_values=None, localize=None, footer=None, exclude_from_export=False, linkify=False, initial_sort_descending=False, ): if not (accessor is None or isinstance(accessor, str) or callable(accessor)): raise TypeError( "accessor must be a string or callable, not %s" % type(accessor).__name__ ) if callable(accessor) and default is not None: raise TypeError("accessor must be string when default is used, not callable") self.accessor = Accessor(accessor) if accessor else None self._default = default self.verbose_name = verbose_name self.visible = visible self.orderable = orderable self.attrs = attrs or getattr(self, "attrs", {}) # massage order_by into an OrderByTuple or None order_by = (order_by,) if isinstance(order_by, str) else order_by self.order_by = OrderByTuple(order_by) if order_by is not None else None if empty_values is not None: self.empty_values = empty_values self.localize = localize self._footer = footer self.exclude_from_export = exclude_from_export link_kwargs = None if callable(linkify) or hasattr(self, "get_url"): link_kwargs = dict(url=linkify if callable(linkify) else self.get_url) elif isinstance(linkify, (dict, tuple)): link_kwargs = dict(reverse_args=linkify) elif linkify is True: link_kwargs = dict(accessor=self.accessor) if link_kwargs is not None: self.link = LinkTransform(attrs=self.attrs.get("a", {}), **link_kwargs) self.initial_sort_descending = initial_sort_descending self.creation_counter = Column.creation_counter Column.creation_counter += 1 @property def default(self): # handle callables return self._default() if callable(self._default) else self._default @property def header(self): """ The value used for the column heading (e.g. inside the ```` tag). By default this returns `~.Column.verbose_name`. :returns: `unicode` or `None` .. note:: This property typically is not accessed directly when a table is rendered. Instead, `.BoundColumn.header` is accessed which in turn accesses this property. This allows the header to fallback to the column name (it is only available on a `.BoundColumn` object hence accessing that first) when this property doesn't return something useful. """ return self.verbose_name def footer(self, bound_column, table): """ Returns the content of the footer, if specified. """ footer_kwargs = {"column": self, "bound_column": bound_column, "table": table} if self._footer is not None: if callable(self._footer): return call_with_appropriate(self._footer, footer_kwargs) else: return self._footer if hasattr(self, "render_footer"): return call_with_appropriate(self.render_footer, footer_kwargs) return "" def render(self, value): """ Returns the content for a specific cell. This method can be overridden by :ref:`table.render_FOO` methods on the table or by subclassing `.Column`. If the value for this cell is in `.empty_values`, this method is skipped and an appropriate default value is rendered instead. Subclasses should set `.empty_values` to ``()`` if they want to handle all values in `.render`. """ return value def value(self, **kwargs): """ Returns the content for a specific cell similarly to `.render` however without any html content. This can be used to get the data in the formatted as it is presented but in a form that could be added to a csv file. The default implementation just calls the `render` function but any subclasses where `render` returns html content should override this method. See `LinkColumn` for an example. """ value = call_with_appropriate(self.render, kwargs) return value def order(self, queryset, is_descending): """ Returns the QuerySet of the table. This method can be overridden by :ref:`table.order_FOO` methods on the table or by subclassing `.Column`; but only overrides if second element in return tuple is True. returns: Tuple (QuerySet, boolean) """ return (queryset, False) @classmethod def from_field(cls, field): """ Return a specialized column for the model field or `None`. Arguments: field (Model Field instance): the field that needs a suitable column Returns: `.Column` object or `None` If the column is not specialized for the given model field, it should return `None`. This gives other columns the opportunity to do better. If the column is specialized, it should return an instance of itself that is configured appropriately for the field. """ # Since this method is inherited by every subclass, only provide a # column if this class was asked directly. if cls is Column: if hasattr(field, "get_related_field"): verbose_name = field.get_related_field().verbose_name else: verbose_name = getattr(field, "verbose_name", field.name) return cls(verbose_name=capfirst(verbose_name)) class BoundColumn: """ A *run-time* version of `.Column`. The difference between `.BoundColumn` and `.Column`, is that `.BoundColumn` objects include the relationship between a `.Column` and a `.Table`. In practice, this means that a `.BoundColumn` knows the *"variable name"* given to the `.Column` when it was declared on the `.Table`. arguments: table (`~.Table`): The table in which this column exists column (`~.Column`): The type of column name (str): The variable name of the column used when defining the `.Table`. In this example the name is ``age``:: class SimpleTable(tables.Table): age = tables.Column() """ def __init__(self, table, column, name): self._table = table self.column = column self.name = name self.link = column.link self.current_value = None def __str__(self): return str(self.header) @property def accessor(self): """ Returns the string used to access data for this column out of the data source. """ return self.column.accessor or Accessor(self.name) @property def attrs(self): """ Proxy to `.Column.attrs` but injects some values of our own. A ``th``, ``td`` and ``tf`` are guaranteed to be defined (irrespective of what is actually defined in the column attrs. This makes writing templates easier. ``tf`` is not actually a HTML tag, but this key name will be used for attributes for column's footer, if the column has one. """ # prepare kwargs for computed_values() kwargs = {"table": self._table, "bound_column": self} # BoundRow.items() sets current_record and current_value when iterating over # the records in a table. if ( getattr(self, "current_record", None) is not None and getattr(self, "current_value", None) is not None ): kwargs.update({"record": self.current_record, "value": self.current_value}) # Start with table's attrs; Only 'th' and 'td' attributes will be used attrs = dict(self._table.attrs) # Update attrs to prefer column's attrs rather than table's attrs.update(dict(self.column.attrs)) # we take the value for 'cell' as the basis for both the th and td attrs cell_attrs = attrs.get("cell", {}) # override with attrs defined specifically for th and td respectively. attrs["th"] = computed_values(attrs.get("th", cell_attrs), kwargs=kwargs) attrs["td"] = computed_values(attrs.get("td", cell_attrs), kwargs=kwargs) attrs["tf"] = computed_values(attrs.get("tf", cell_attrs), kwargs=kwargs) # wrap in AttributeDict attrs["th"] = AttributeDict(attrs["th"]) attrs["td"] = AttributeDict(attrs["td"]) attrs["tf"] = AttributeDict(attrs["tf"]) # Override/add classes attrs["th"]["class"] = self.get_th_class(attrs["th"]) attrs["td"]["class"] = self.get_td_class(attrs["td"]) attrs["tf"]["class"] = self.get_td_class(attrs["tf"]) return attrs def _get_cell_class(self, attrs): """ Return a set of the classes from the class key in ``attrs``. """ classes = attrs.get("class", None) classes = set() if classes is None else set(classes.split(" ")) return self._table.get_column_class_names(classes, self) def get_td_class(self, td_attrs): """ Returns the HTML class attribute for a data cell in this column """ classes = sorted(self._get_cell_class(td_attrs)) return None if len(classes) == 0 else " ".join(classes) def get_th_class(self, th_attrs): """ Returns the HTML class attribute for a header cell in this column """ classes = self._get_cell_class(th_attrs) # add classes for ordering ordering_class = th_attrs.get("_ordering", {}) if self.orderable: classes.add(ordering_class.get("orderable", "orderable")) if self.is_ordered: classes.add( ordering_class.get("descending", "desc") if self.order_by_alias.is_descending else ordering_class.get("ascending", "asc") ) return None if len(classes) == 0 else " ".join(sorted(classes)) @property def default(self): """ Returns the default value for this column. """ value = self.column.default if value is None: value = self._table.default return value @property def header(self): """ The value that should be used in the header cell for this column. """ # favour Column.header column_header = self.column.header if column_header: return column_header # fall back to automatic best guess return self.verbose_name @property def footer(self): return call_with_appropriate( self.column.footer, {"bound_column": self, "table": self._table} ) def has_footer(self): return self.column._footer is not None or hasattr(self.column, "render_footer") @property def order_by(self): """ Returns an `.OrderByTuple` of appropriately prefixed data source keys used to sort this column. See `.order_by_alias` for details. """ if self.column.order_by is not None: order_by = self.column.order_by else: # default to using column accessor as data source sort key order_by = OrderByTuple((self.accessor,)) return order_by.opposite if self.order_by_alias.is_descending else order_by @property def order_by_alias(self): """ Returns an `OrderBy` describing the current state of ordering for this column. The following attempts to explain the difference between `order_by` and `.order_by_alias`. `.order_by_alias` returns and `.OrderBy` instance that's based on the *name* of the column, rather than the keys used to order the table data. Understanding the difference is essential. Having an alias *and* a keys version is necessary because an N-tuple (of data source keys) can be used by the column to order the data, and it is ambiguous when mapping from N-tuple to column (since multiple columns could use the same N-tuple). The solution is to use order by *aliases* (which are really just prefixed column names) that describe the ordering *state* of the column, rather than the specific keys in the data source should be ordered. e.g.:: >>> class SimpleTable(tables.Table): ... name = tables.Column(order_by=("firstname", "last_name")) ... >>> table = SimpleTable([], order_by=('-name', )) >>> table.columns["name"].order_by_alias "-name" >>> table.columns["name"].order_by ("-first_name", "-last_name") The `OrderBy` returned has been patched to include an extra attribute ``next``, which returns a version of the alias that would be transitioned to if the user toggles sorting on this column, for example:: not sorted -> ascending ascending -> descending descending -> ascending This is useful otherwise in templates you'd need something like:: {% if column.is_ordered %} {% querystring table.prefixed_order_by_field=column.order_by_alias.opposite %} {% else %} {% querystring table.prefixed_order_by_field=column.order_by_alias %} {% endif %} """ order_by = OrderBy((self._table.order_by or {}).get(self.name, self.name)) order_by.next = order_by.opposite if self.is_ordered else order_by if self.column.initial_sort_descending and not self.is_ordered: order_by.next = order_by.opposite return order_by @property def is_ordered(self): return self.name in (self._table.order_by or ()) @property def orderable(self): """ Return a `bool` depending on whether this column supports ordering. """ if self.column.orderable is not None: return self.column.orderable return self._table.orderable @property def verbose_name(self): """ Return the verbose name for this column. In order of preference, this will return: 1) The column's explicitly defined `verbose_name` 2) The model's `verbose_name` with the first letter capitalized (if applicable) 3) Fall back to the column name, with first letter capitalized. Any `verbose_name` that was not passed explicitly in the column definition is returned with the first character capitalized in keeping with the Django convention of `verbose_name` being defined in lowercase and uppercased as needed by the application. If the table is using `QuerySet` data, then use the corresponding model field's `~.db.Field.verbose_name`. If it is traversing a relationship, then get the last field in the accessor (i.e. stop when the relationship turns from ORM relationships to object attributes [e.g. person.upper should stop at person]). """ # Favor an explicit defined verbose_name if self.column.verbose_name is not None: return self.column.verbose_name # This is our reasonable fall back, should the next section not result # in anything useful. name = self.name.replace("_", " ") # Try to use a model field's verbose_name model = self._table.data.model if model: field = Accessor(self.accessor).get_field(model) if field: if hasattr(field, "field"): name = field.field.verbose_name else: name = getattr(field, "verbose_name", field.name) # If verbose_name was mark_safe()'d, return intact to keep safety if isinstance(name, SafeData): return name return capfirst(name) @property def visible(self): """Returns a `bool` depending on whether this column is visible.""" return self.column.visible @property def localize(self): """Returns `True`, `False` or `None` as described in ``Column.localize``""" return self.column.localize class BoundColumns: """ Container for spawning `.BoundColumn` objects. This is bound to a table and provides its `.Table.columns` property. It provides access to those columns in different ways (iterator, item-based, filtered and unfiltered etc), stuff that would not be possible with a simple iterator in the table class. A `BoundColumns` object is a container for holding `BoundColumn` objects. It provides methods that make accessing columns easier than if they were stored in a `list` or `dict`. `Columns` has a similar API to a `dict` (it actually uses a `~collections.OrderedDict` internally). At the moment you'll only come across this class when you access a `.Table.columns` property. Arguments: table (`.Table`): the table containing the columns """ def __init__(self, table, base_columns): self._table = table self.columns = OrderedDict() for name, column in base_columns.items(): self.columns[name] = bound_column = BoundColumn(table, column, name) bound_column.render = getattr(table, "render_" + name, column.render) # How the value is defined: 1. value_ 2. render_ 3. column.value. bound_column.value = getattr( table, "value_" + name, getattr(table, "render_" + name, column.value) ) bound_column.order = getattr(table, "order_" + name, column.order) def iternames(self): return (name for name, column in self.iteritems()) def names(self): return list(self.iternames()) def iterall(self): """ Return an iterator that exposes all `.BoundColumn` objects, regardless of visibility or sortability. """ return (column for name, column in self.iteritems()) def all(self): return list(self.iterall()) def iteritems(self): """ Return an iterator of ``(name, column)`` pairs (where ``column`` is a `BoundColumn`). This method is the mechanism for retrieving columns that takes into consideration all of the ordering and filtering modifiers that a table supports (e.g. `~Table.Meta.exclude` and `~Table.Meta.sequence`). """ for name in self._table.sequence: if name not in self._table.exclude: yield (name, self.columns[name]) def items(self): return list(self.iteritems()) def iterorderable(self): """ Same as `BoundColumns.all` but only returns orderable columns. This is useful in templates, where iterating over the full set and checking ``{% if column.ordarable %}`` can be problematic in conjunction with e.g. ``{{ forloop.last }}`` (the last column might not be the actual last that is rendered). """ return (x for x in self.iterall() if x.orderable) def itervisible(self): """ Same as `.iterorderable` but only returns visible `.BoundColumn` objects. This is geared towards table rendering. """ return (x for x in self.iterall() if x.visible) def hide(self, name): """ Hide a column. Arguments: name(str): name of the column """ self.columns[name].column.visible = False def show(self, name): """ Show a column otherwise hidden. Arguments: name(str): name of the column """ self.columns[name].column.visible = True def __iter__(self): """Convenience API, alias of `.itervisible`.""" return self.itervisible() def __contains__(self, item): """ Check if a column is contained within a `BoundColumns` object. *item* can either be a `~.BoundColumn` object, or the name of a column. """ if isinstance(item, str): return item in self.iternames() else: # let's assume we were given a column return item in self.iterall() def __len__(self): """Return how many `~.BoundColumn` objects are contained (and visible).""" return len(list(self.itervisible())) def __getitem__(self, index): """ Retrieve a specific `~.BoundColumn` object. *index* can either be 0-indexed or the name of a column .. code-block:: python columns['speed'] # returns a bound column with name 'speed' columns[0] # returns the first column """ if isinstance(index, int): try: return next(islice(self.iterall(), index, index + 1)) except StopIteration: raise IndexError elif isinstance(index, str): for column in self.iterall(): if column.name == index: return column raise KeyError( "Column with name '{}' does not exist; choices are: {}".format(index, self.names()) ) else: raise TypeError( "Column indices must be integers or str, not {}".format(type(index).__name__) ) django-tables2-2.1.1/django_tables2/columns/templatecolumn.py0000644000175000017500000000573613542204315023624 0ustar josephjosephfrom django.template import Context, Template from django.template.loader import get_template from django.utils.html import strip_tags from .base import Column, library @library.register class TemplateColumn(Column): """ A subclass of `.Column` that renders some template code to use as the cell value. Arguments: template_code (str): template code to render template_name (str): name of the template to render extra_context (dict): optional extra template context A `~django.template.Template` object is created from the *template_code* or *template_name* and rendered with a context containing: - *record* -- data record for the current row - *value* -- value from `record` that corresponds to the current column - *default* -- appropriate default value to use as fallback. - *row_counter* -- The number of the row this cell is being rendered in. - any context variables passed using the `extra_context` argument to `TemplateColumn`. Example: .. code-block:: python class ExampleTable(tables.Table): foo = tables.TemplateColumn("{{ record.bar }}") # contents of `myapp/bar_column.html` is `{{ label }}: {{ value }}` bar = tables.TemplateColumn(template_name="myapp/name2_column.html", extra_context={"label": "Label"}) Both columns will have the same output. """ empty_values = () def __init__(self, template_code=None, template_name=None, extra_context=None, **extra): super().__init__(**extra) self.template_code = template_code self.template_name = template_name self.extra_context = extra_context or {} if not self.template_code and not self.template_name: raise ValueError("A template must be provided") def render(self, record, table, value, bound_column, **kwargs): # If the table is being rendered using `render_table`, it hackily # attaches the context to the table as a gift to `TemplateColumn`. context = getattr(table, "context", Context()) context.update(self.extra_context) context.update( { "default": bound_column.default, "column": bound_column, "record": record, "value": value, "row_counter": kwargs["bound_row"].row_counter, } ) try: if self.template_code: return Template(self.template_code).render(context) else: return get_template(self.template_name).render(context.flatten()) finally: context.pop() def value(self, **kwargs): """ The value returned from a call to `value()` on a `TemplateColumn` is the rendered template with `django.utils.html.strip_tags` applied. """ html = super().value(**kwargs) return strip_tags(html) if isinstance(html, str) else html django-tables2-2.1.1/django_tables2/columns/datetimecolumn.py0000644000175000017500000000203613542204315023573 0ustar josephjosephfrom django.db import models from django.utils.text import capfirst from .base import library from .templatecolumn import TemplateColumn @library.register class DateTimeColumn(TemplateColumn): """ A column that renders `datetime` instances in the local timezone. Arguments: format (str): format string for datetime (optional). Note that *format* uses Django's `date` template tag syntax. short (bool): if `format` is not specified, use Django's ``SHORT_DATETIME_FORMAT``, else ``DATETIME_FORMAT`` """ def __init__(self, format=None, short=True, *args, **kwargs): if format is None: format = "SHORT_DATETIME_FORMAT" if short else "DATETIME_FORMAT" template = '{{ value|date:"%s"|default:default }}' % format super().__init__(template_code=template, *args, **kwargs) @classmethod def from_field(cls, field): if isinstance(field, models.DateTimeField): return cls(verbose_name=capfirst(field.verbose_name)) django-tables2-2.1.1/django_tables2/columns/__init__.py0000644000175000017500000000156713542204315022330 0ustar josephjosephfrom .base import BoundColumn, BoundColumns, Column, library from .booleancolumn import BooleanColumn from .checkboxcolumn import CheckBoxColumn from .datecolumn import DateColumn from .datetimecolumn import DateTimeColumn from .emailcolumn import EmailColumn from .filecolumn import FileColumn from .jsoncolumn import JSONColumn from .linkcolumn import LinkColumn, RelatedLinkColumn from .manytomanycolumn import ManyToManyColumn from .templatecolumn import TemplateColumn from .timecolumn import TimeColumn from .urlcolumn import URLColumn __all__ = ( "library", "BoundColumn", "BoundColumns", "Column", "BooleanColumn", "CheckBoxColumn", "DateColumn", "DateTimeColumn", "EmailColumn", "FileColumn", "JSONColumn", "LinkColumn", "ManyToManyColumn", "RelatedLinkColumn", "TemplateColumn", "URLColumn", "TimeColumn", ) django-tables2-2.1.1/django_tables2/columns/booleancolumn.py0000644000175000017500000000505013542204315023415 0ustar josephjosephfrom django.db import models from django.utils.html import escape, format_html from django.utils.text import capfirst from ..utils import AttributeDict from .base import Column, library @library.register class BooleanColumn(Column): """ A column suitable for rendering boolean data. Arguments: null (bool): is `None` different from `False`? yesno (str): comma separated values string or 2-tuple to display for True/False values. Rendered values are wrapped in a ```` to allow customization by using CSS. By default the span is given the class ``true``, ``false``. In addition to *attrs* keys supported by `~.Column`, the following are available: - ``span`` -- adds attributes to the ```` tag """ def __init__(self, null=False, yesno="✔,✘", **kwargs): self.yesno = yesno.split(",") if isinstance(yesno, str) else tuple(yesno) if not null: kwargs["empty_values"] = () super().__init__(**kwargs) def _get_bool_value(self, record, value, bound_column): # If record is a model, we need to check if it has choices defined. if hasattr(record, "_meta"): field = bound_column.accessor.get_field(record) # If that's the case, we need to inverse lookup the value to convert # to a boolean we can use. if hasattr(field, "choices") and field.choices is not None and len(field.choices) > 0: value = next(val for val, name in field.choices if name == value) value = bool(value) return value def render(self, value, record, bound_column): value = self._get_bool_value(record, value, bound_column) text = self.yesno[int(not value)] attrs = {"class": str(value).lower()} attrs.update(self.attrs.get("span", {})) return format_html("{}", AttributeDict(attrs).as_html(), escape(text)) def value(self, record, value, bound_column): """ Returns the content for a specific cell similarly to `.render` however without any html content. """ value = self._get_bool_value(record, value, bound_column) return str(value) @classmethod def from_field(cls, field): if isinstance(field, models.NullBooleanField): return cls(verbose_name=capfirst(field.verbose_name), null=True) if isinstance(field, models.BooleanField): null = getattr(field, "null", False) return cls(verbose_name=capfirst(field.verbose_name), null=null) django-tables2-2.1.1/django_tables2/columns/emailcolumn.py0000644000175000017500000000221213542204315023062 0ustar josephjosephfrom django.db import models from django.utils.text import capfirst from .base import library from .linkcolumn import BaseLinkColumn @library.register class EmailColumn(BaseLinkColumn): """ Render email addresses to `mailto:`-links. Arguments: attrs (dict): HTML attributes that are added to the rendered ``...`` tag. text: Either static text, or a callable. If set, this will be used to render the text inside link instead of the value. Example:: # models.py class Person(models.Model): name = models.CharField(max_length=200) email = models.EmailField() # tables.py class PeopleTable(tables.Table): name = tables.Column() email = tables.EmailColumn() # result # [...]email@example.com """ def get_url(self, value): return "mailto:{}".format(value) @classmethod def from_field(cls, field): if isinstance(field, models.EmailField): return cls(verbose_name=capfirst(field.verbose_name)) django-tables2-2.1.1/django_tables2/columns/datecolumn.py0000644000175000017500000000176113542204315022720 0ustar josephjosephfrom django.db import models from django.utils.text import capfirst from .base import library from .templatecolumn import TemplateColumn @library.register class DateColumn(TemplateColumn): """ A column that renders dates in the local timezone. Arguments: format (str): format string in same format as Django's ``date`` template filter (optional) short (bool): if `format` is not specified, use Django's ``SHORT_DATE_FORMAT`` setting, otherwise use ``DATE_FORMAT`` """ def __init__(self, format=None, short=True, *args, **kwargs): if format is None: format = "SHORT_DATE_FORMAT" if short else "DATE_FORMAT" template = '{{ value|date:"%s"|default:default }}' % format super().__init__(template_code=template, *args, **kwargs) @classmethod def from_field(cls, field): if isinstance(field, models.DateField): return cls(verbose_name=capfirst(field.verbose_name)) django-tables2-2.1.1/django_tables2/columns/urlcolumn.py0000644000175000017500000000173413542204315022605 0ustar josephjosephfrom django.db import models from django.utils.text import capfirst from .base import library from .linkcolumn import BaseLinkColumn @library.register class URLColumn(BaseLinkColumn): """ Renders URL values as hyperlinks. Arguments: text (str or callable): Either static text, or a callable. If set, this will be used to render the text inside link instead of value (default) attrs (dict): Additional attributes for the ```` tag Example:: >>> class CompaniesTable(tables.Table): ... link = tables.URLColumn() ... >>> table = CompaniesTable([{"link": "http://google.com"}]) >>> table.rows[0].get_cell("link") 'http://google.com' """ def get_url(self, value): return value @classmethod def from_field(cls, field): if isinstance(field, models.URLField): return cls(verbose_name=capfirst(field.verbose_name)) django-tables2-2.1.1/django_tables2/paginators.py0000644000175000017500000000742413542204315021256 0ustar josephjosephfrom django.core.paginator import EmptyPage, Page, PageNotAnInteger, Paginator from django.utils.translation import gettext as _ class LazyPaginator(Paginator): """ Implement lazy pagination, preventing any count() queries. By default, for any valid page, the total number of pages for the paginator will be - `current + 1` if the number of records fetched for the current page offset is bigger than the number of records per page. - `current` if the number of records fetched is less than the number of records per page. The number of additional records fetched can be adjusted using `look_ahead`, which defaults to 1 page. If you like to provide a little more extra information on how much pages follow the current page, you can use a higher value. .. note:: The number of records fetched for each page is `per_page * look_ahead + 1`, so increasing the value for `look_ahead` makes the view a bit more expensive. So:: paginator = LazyPaginator(range(10000), 10) >>> paginator.page(1).object_list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] >>> paginator.num_pages 2 >>> paginator.page(10).object_list [91, 92, 93, 94, 95, 96, 97, 98, 99, 100] >>> paginator.num_pages 11 >>> paginator.page(1000).object_list [9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999] >>> paginator.num_pages 1000 Usage with `~.SingleTableView`:: class UserListView(SingleTableView): table_class = UserTable table_data = User.objects.all() pagination_class = LazyPaginator Or with `~.RequestConfig`:: RequestConfig(paginate={"paginator_class": LazyPaginator}).configure(table) .. versionadded :: 2.0.0 """ look_ahead = 1 def __init__(self, object_list, per_page, look_ahead=None, **kwargs): self._num_pages = None if look_ahead is not None: self.look_ahead = look_ahead super().__init__(object_list, per_page, **kwargs) def validate_number(self, number): """Validate the given 1-based page number.""" try: if isinstance(number, float) and not number.is_integer(): raise ValueError number = int(number) except (TypeError, ValueError): raise PageNotAnInteger(_("That page number is not an integer")) if number < 1: raise EmptyPage(_("That page number is less than 1")) return number def page(self, number): number = self.validate_number(number) bottom = (number - 1) * self.per_page top = bottom + self.per_page # Retrieve more objects to check if there is a next page. look_ahead_items = (self.look_ahead - 1) * self.per_page + 1 objects = list(self.object_list[bottom : top + self.orphans + look_ahead_items]) objects_count = len(objects) if objects_count > (self.per_page + self.orphans): # If another page is found, increase the total number of pages. self._num_pages = number + (objects_count // self.per_page) # In any case, return only objects for this page. objects = objects[: self.per_page] elif (number != 1) and (objects_count <= self.orphans): raise EmptyPage(_("That page contains no results")) else: # This is the last page. self._num_pages = number return Page(objects, number, self) def _get_count(self): raise NotImplementedError count = property(_get_count) def _get_num_pages(self): return self._num_pages num_pages = property(_get_num_pages) def _get_page_range(self): raise NotImplementedError page_range = property(_get_page_range) django-tables2-2.1.1/django_tables2/locale/0000755000175000017500000000000013542204315017765 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/zh_Hans/0000755000175000017500000000000013542204315021357 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/zh_Hans/LC_MESSAGES/0000755000175000017500000000000013542204315023144 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/zh_Hans/LC_MESSAGES/django.po0000644000175000017500000000162213542204315024747 0ustar josephjoseph# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2018-03-19 16:20+0800\n" "Last-Translator: Zhong Chang<726608501@qq.com>\n" "Language-Team: Simplified Chinese\n" "Language: zh_Hans\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "上一页" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "下一页" django-tables2-2.1.1/django_tables2/locale/zh_Hans/LC_MESSAGES/django.mo0000644000175000017500000000064513542204315024750 0ustar josephjoseph4L`af!o  nextpreviousProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2018-03-19 16:20+0800 Last-Translator: Zhong Chang<726608501@qq.com> Language-Team: Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 下一页上一页django-tables2-2.1.1/django_tables2/locale/nb/0000755000175000017500000000000013542204315020364 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/nb/LC_MESSAGES/0000755000175000017500000000000013542204315022151 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/nb/LC_MESSAGES/django.po0000644000175000017500000000143213542204315023753 0ustar josephjoseph# This file is distributed under the same license as the django-tables2 package # msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2016-07-22 11:11+0200\n" "Last-Translator: Andreas TollånesLanguage-Team: Norwegian Bokmal \n" "Language: nb\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "forrige" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "neste" django-tables2-2.1.1/django_tables2/locale/nb/LC_MESSAGES/django.mo0000644000175000017500000000067613542204315023761 0ustar josephjoseph4L`af@onextpreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2016-07-22 11:11+0200 Last-Translator: Andreas TollånesLanguage-Team: Norwegian Bokmal Language: nb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nesteforrigedjango-tables2-2.1.1/django_tables2/locale/ru/0000755000175000017500000000000013542204315020413 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/ru/LC_MESSAGES/0000755000175000017500000000000013542204315022200 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/ru/LC_MESSAGES/django.po0000644000175000017500000000207713542204315024010 0ustar josephjoseph# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Andrii Pryz \n" "Language-Team: Russian \n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "πредыдущая" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "cледующая" django-tables2-2.1.1/django_tables2/locale/ru/LC_MESSAGES/django.mo0000644000175000017500000000113713542204315024001 0ustar josephjoseph4L`afo8JnextpreviousProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: Andrii Pryz Language-Team: RU Language: ru MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); cледующаяπредыдущаяdjango-tables2-2.1.1/django_tables2/locale/de/0000755000175000017500000000000013542204315020355 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/de/LC_MESSAGES/0000755000175000017500000000000013542204315022142 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/de/LC_MESSAGES/django.po0000644000175000017500000000170413542204315023746 0ustar josephjoseph# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2015-04-09 12:45+0100\n" "Last-Translator: Tim Schneider \n" "Language-Team: \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.7.5\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "zurück" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "weiter" django-tables2-2.1.1/django_tables2/locale/de/LC_MESSAGES/django.mo0000644000175000017500000000076113542204315023745 0ustar josephjoseph4L`afronextpreviousProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2015-04-09 12:45+0100 Last-Translator: Tim Schneider Language-Team: Language: de MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Generator: Poedit 1.7.5 weiterzurückdjango-tables2-2.1.1/django_tables2/locale/en/0000755000175000017500000000000013542204315020367 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/en/LC_MESSAGES/0000755000175000017500000000000013542204315022154 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/en/LC_MESSAGES/django.po0000644000175000017500000000145413542204315023762 0ustar josephjoseph# This file is distributed under the same license as the django-tables2 package # msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2011-11-06 10:41+1000\n" "Last-Translator: Bradley Ayers \n" "Language-Team: English \n" "Language: en_US\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "previous" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "next" django-tables2-2.1.1/django_tables2/locale/en/LC_MESSAGES/django.mo0000644000175000017500000000066213542204315023757 0ustar josephjoseph,<PQNZpreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2011-11-06 10:41+1000 Last-Translator: Bradley Ayers Language-Team: English Language: en MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit previousdjango-tables2-2.1.1/django_tables2/locale/pt_BR/0000755000175000017500000000000013542204315020773 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/pt_BR/LC_MESSAGES/0000755000175000017500000000000013542204315022560 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/pt_BR/LC_MESSAGES/django.po0000644000175000017500000000167013542204315024366 0ustar josephjoseph# This file is distributed under the same license as the django-tables2 package # Fabio C. Barrionuevo da Luz , 2014. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.14.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2014-02-02 00:44-0300\n" "Last-Translator: Fabio C. Barrionuevo da Luz \n" "Language-Team: Portuguese (Brazil) \n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "anterior" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "próximo" django-tables2-2.1.1/django_tables2/locale/pt_BR/LC_MESSAGES/django.mo0000644000175000017500000000101713542204315024356 0ustar josephjoseph4L`afonextpreviousProject-Id-Version: 0.14.0 Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2014-02-02 00:44-0300 Last-Translator: Fabio C. Barrionuevo da Luz Language-Team: Portuguese (Brazil) Language: pt_BR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1); próximoanteriordjango-tables2-2.1.1/django_tables2/locale/uk/0000755000175000017500000000000013542204315020404 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/uk/LC_MESSAGES/0000755000175000017500000000000013542204315022171 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/uk/LC_MESSAGES/django.po0000644000175000017500000000207513542204315023777 0ustar josephjoseph# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Andrii Pryz \n" "Language-Team: Ukrainian \n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "πопередня" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "hаступна" django-tables2-2.1.1/django_tables2/locale/uk/LC_MESSAGES/django.mo0000644000175000017500000000113313542204315023766 0ustar josephjoseph4L`afo8HnextpreviousProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: Andrii Pryz Language-Team: UK Language: uk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); hаступнаπопередняdjango-tables2-2.1.1/django_tables2/locale/sv/0000755000175000017500000000000013542204315020415 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/sv/LC_MESSAGES/0000755000175000017500000000000013542204315022202 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/sv/LC_MESSAGES/django.po0000644000175000017500000000145113542204315024005 0ustar josephjoseph# This file is distributed under the same license as the django-tables2 package # msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2014-12-04 10:25+0100\n" "Last-Translator: Petter Jönsson \n" "Language-Team: Swedish \n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "föregående" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "nästa" django-tables2-2.1.1/django_tables2/locale/sv/LC_MESSAGES/django.mo0000644000175000017500000000071413542204315024003 0ustar josephjoseph4L`afHo nextpreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2014-12-04 10:25+0100 Last-Translator: Petter Jönsson Language-Team: Swedish Language: sv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nästaföregåendedjango-tables2-2.1.1/django_tables2/locale/pt_PT/0000755000175000017500000000000013542204315021013 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/pt_PT/LC_MESSAGES/0000755000175000017500000000000013542204315022600 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/pt_PT/LC_MESSAGES/django.po0000644000175000017500000000147413542204315024410 0ustar josephjoseph# This file is distributed under the same license as the django-tables2 package # msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2011-11-06 10:41+1000\n" "Last-Translator: Bradley Ayers \n" "Language-Team: European Portuguese \n" "Language: pt_PT\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "Anterior" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "seguinte" django-tables2-2.1.1/django_tables2/locale/pt_PT/LC_MESSAGES/django.mo0000644000175000017500000000072013542204315024376 0ustar josephjoseph4L`afNonextpreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2011-11-06 10:41+1000 Last-Translator: Bradley Ayers Language-Team: English Language: en MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit seguinteAnteriordjango-tables2-2.1.1/django_tables2/locale/hu/0000755000175000017500000000000013542204315020401 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/hu/LC_MESSAGES/0000755000175000017500000000000013542204315022166 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/hu/LC_MESSAGES/django.po0000644000175000017500000000152113542204315023767 0ustar josephjoseph# This file is distributed under the same license as the django-tables2 package # msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2017-08-13 14:19+0200\n" "Last-Translator: Miklos Horvath \n" "Language-Team: Hungarian \n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.8.12\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "előző" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "következő" django-tables2-2.1.1/django_tables2/locale/hu/LC_MESSAGES/django.mo0000644000175000017500000000076013542204315023770 0ustar josephjoseph4L`aflo nextpreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2017-08-13 14:19+0200 Last-Translator: Miklos Horvath Language-Team: Hungarian Language: en MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Generator: Poedit 1.8.12 következőelőződjango-tables2-2.1.1/django_tables2/locale/it/0000755000175000017500000000000013542204315020401 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/it/LC_MESSAGES/0000755000175000017500000000000013542204315022166 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/it/LC_MESSAGES/django.po0000644000175000017500000000151313542204315023770 0ustar josephjoseph# This file is distributed under the same license as the django-tables2 package msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2016-04-14 11:21+0200\n" "Last-Translator: Paolo Dina \n" "Language-Team: Italian \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "indietro" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "avanti" django-tables2-2.1.1/django_tables2/locale/it/LC_MESSAGES/django.mo0000644000175000017500000000075413542204315023773 0ustar josephjoseph4L`aflonextpreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2016-04-14 11:21+0200 Last-Translator: Paolo Dina Language-Team: Italian Language: it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); avantiindietrodjango-tables2-2.1.1/django_tables2/locale/el/0000755000175000017500000000000013542204315020365 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/el/LC_MESSAGES/0000755000175000017500000000000013542204315022152 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/el/LC_MESSAGES/django.po0000644000175000017500000000151713542204315023760 0ustar josephjoseph# This file is distributed under the same license as the django-tables2 package # #, fuzzy msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2013-03-19 21:56+0200\n" "Last-Translator: Serafeim Papastefanos \n" "Language-Team: el \n" "Language: el\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "προηγούμενη" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "eπόμενη" django-tables2-2.1.1/django_tables2/locale/el/LC_MESSAGES/django.mo0000644000175000017500000000074713542204315023761 0ustar josephjoseph4L`afRo nextpreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2013-03-19 21:56+0200 Last-Translator: Serafeim Papastefanos Language-Team: el Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit eπόμενηπροηγούμενηdjango-tables2-2.1.1/django_tables2/locale/fr/0000755000175000017500000000000013542204315020374 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/fr/LC_MESSAGES/0000755000175000017500000000000013542204315022161 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/fr/LC_MESSAGES/django.po0000644000175000017500000000170113542204315023762 0ustar josephjoseph# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: fr \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "précédent" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "suivant" django-tables2-2.1.1/django_tables2/locale/fr/LC_MESSAGES/django.mo0000644000175000017500000000075513542204315023767 0ustar josephjoseph4L`afio nextpreviousProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1) suivantprécédentdjango-tables2-2.1.1/django_tables2/locale/cs/0000755000175000017500000000000013542204315020372 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/cs/LC_MESSAGES/0000755000175000017500000000000013542204315022157 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/cs/LC_MESSAGES/django.po0000644000175000017500000000174613542204315023771 0ustar josephjoseph# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2018-01-22 08:21+0100\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "předchozí" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "další" django-tables2-2.1.1/django_tables2/locale/cs/LC_MESSAGES/django.mo0000644000175000017500000000102513542204315023754 0ustar josephjoseph4L`afo nextpreviousProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2018-01-22 08:21+0100 Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2; dalšípředchozídjango-tables2-2.1.1/django_tables2/locale/pl/0000755000175000017500000000000013542204315020400 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/pl/LC_MESSAGES/0000755000175000017500000000000013542204315022165 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/pl/LC_MESSAGES/django.po0000644000175000017500000000205013542204315023764 0ustar josephjoseph# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2013-08-22 09:57+0100\n" "Last-Translator: Michał Pasternak \n" "Language-Team: Polish \n" "Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2);\n" "X-Generator: Poedit 1.5.5\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "poprzednia" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "następna" django-tables2-2.1.1/django_tables2/locale/pl/LC_MESSAGES/django.mo0000644000175000017500000000112213542204315023760 0ustar josephjoseph4L`afo = GnextpreviousProject-Id-Version: 0.1 Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2013-08-22 09:57+0100 Last-Translator: Michał Pasternak Language-Team: PL Language: polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); X-Generator: Poedit 1.5.5 następnapoprzedniadjango-tables2-2.1.1/django_tables2/locale/es/0000755000175000017500000000000013542204315020374 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/es/LC_MESSAGES/0000755000175000017500000000000013542204315022161 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/es/LC_MESSAGES/django.po0000644000175000017500000000145413542204315023767 0ustar josephjoseph# This file is distributed under the same license as the django-tables2 package # msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2013-08-21 07:06-0500\n" "Last-Translator: Pablo Martín \n" "Language-Team: LANGUAGE \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "anterior" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "siguiente" django-tables2-2.1.1/django_tables2/locale/es/LC_MESSAGES/django.mo0000644000175000017500000000071713542204315023765 0ustar josephjoseph4L`afLo nextpreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2013-08-21 07:06-0500 Last-Translator: Pablo Martín Language-Team: LANGUAGE Language: es MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit siguienteanteriordjango-tables2-2.1.1/django_tables2/locale/nl/0000755000175000017500000000000013542204315020376 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/nl/LC_MESSAGES/0000755000175000017500000000000013542204315022163 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/locale/nl/LC_MESSAGES/django.po0000644000175000017500000000153013542204315023764 0ustar josephjoseph# This file is distributed under the same license as the django-tables2 package msgid "" msgstr "" "Project-Id-Version: django-tables2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 10:06+0200\n" "PO-Revision-Date: 2016-04-19 10:21+0200\n" "Last-Translator: Jan Pieter Waagmeester \n" "Language-Team: Dutch \n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: templates/django_tables2/bootstrap.html:64 #: templates/django_tables2/bootstrap4.html:64 #: templates/django_tables2/table.html:61 msgid "previous" msgstr "vorige" #: templates/django_tables2/bootstrap.html:89 #: templates/django_tables2/bootstrap4.html:82 #: templates/django_tables2/table.html:82 msgid "next" msgstr "volgende" django-tables2-2.1.1/django_tables2/locale/nl/LC_MESSAGES/django.mo0000644000175000017500000000077113542204315023767 0ustar josephjoseph4L`afyonextpreviousProject-Id-Version: django-tables2 Report-Msgid-Bugs-To: POT-Creation-Date: 2018-04-12 10:06+0200 PO-Revision-Date: 2016-04-19 10:21+0200 Last-Translator: Jan Pieter Waagmeester Language-Team: Dutch Language: nl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); volgendevorigedjango-tables2-2.1.1/django_tables2/data.py0000644000175000017500000001657413542204315020026 0ustar josephjosephimport warnings from django.utils.functional import cached_property from .utils import OrderBy, OrderByTuple, segment class TableData: """ Base class for table data containers. """ def __init__(self, data): self.data = data def __getitem__(self, key): """ Slicing returns a new `.TableData` instance, indexing returns a single record. """ return self.data[key] def __iter__(self): """ for ... in ... default to using this. There's a bug in Django 1.3 with indexing into QuerySets, so this side-steps that problem (as well as just being a better way to iterate). """ return iter(self.data) def set_table(self, table): """ `Table.__init__` calls this method to inject an instance of itself into the `TableData` instance. Good place to do additional checks if Table and TableData instance will work together properly. """ self.table = table @property def model(self): return getattr(self.data, "model", None) @property def ordering(self): return None @property def verbose_name(self): return "item" @property def verbose_name_plural(self): return "items" @staticmethod def from_data(data): # allow explicit child classes of TableData to be passed to Table() if isinstance(data, TableData): return data if TableQuerysetData.validate(data): return TableQuerysetData(data) elif TableListData.validate(data): return TableListData(list(data)) raise ValueError( "data must be QuerySet-like (have count() and order_by()) or support" " list(data) -- {} has neither".format(type(data).__name__) ) class TableListData(TableData): """ Table data container for a list of dicts, for example:: [ {'name': 'John', 'age': 20}, {'name': 'Brian', 'age': 25} ] .. note:: Other structures might have worked in the past, but are not explicitly supported or tested. """ @staticmethod def validate(data): """ Validates `data` for use in this container """ return hasattr(data, "__iter__") or ( hasattr(data, "__len__") and hasattr(data, "__getitem__") ) def __len__(self): return len(self.data) @property def verbose_name(self): return getattr(self.data, "verbose_name", super().verbose_name) @property def verbose_name_plural(self): return getattr(self.data, "verbose_name_plural", super().verbose_name_plural) def order_by(self, aliases): """ Order the data based on order by aliases (prefixed column names) in the table. Arguments: aliases (`~.utils.OrderByTuple`): optionally prefixed names of columns ('-' indicates descending order) in order of significance with regard to data ordering. """ accessors = [] for alias in aliases: bound_column = self.table.columns[OrderBy(alias).bare] # bound_column.order_by reflects the current ordering applied to # the table. As such we need to check the current ordering on the # column and use the opposite if it doesn't match the alias prefix. if alias[0] != bound_column.order_by_alias[0]: accessors += bound_column.order_by.opposite else: accessors += bound_column.order_by self.data.sort(key=OrderByTuple(accessors).key) class TableQuerysetData(TableData): """ Table data container for a queryset. """ @staticmethod def validate(data): """ Validates `data` for use in this container """ return ( hasattr(data, "count") and callable(data.count) and hasattr(data, "order_by") and callable(data.order_by) ) def __len__(self): """Cached data length""" if not hasattr(self, "_length") or self._length is None: if hasattr(self.table, "paginator"): # for paginated tables, use QuerySet.count() as we are interested in total number of records. self._length = self.data.count() else: # for non-paginated tables, use the length of the QuerySet self._length = len(self.data) return self._length def set_table(self, table): super().set_table(table) if self.model and getattr(table._meta, "model", None) and self.model != table._meta.model: warnings.warn( "Table data is of type {} but {} is specified in Table.Meta.model".format( self.model, table._meta.model ) ) @property def ordering(self): """ Returns the list of order by aliases that are enforcing ordering on the data. If the data is unordered, an empty sequence is returned. If the ordering can not be determined, `None` is returned. This works by inspecting the actual underlying data. As such it's only supported for querysets. """ aliases = {} for bound_column in self.table.columns: aliases[bound_column.order_by_alias] = bound_column.order_by try: return next(segment(self.data.query.order_by, aliases)) except StopIteration: pass def order_by(self, aliases): """ Order the data based on order by aliases (prefixed column names) in the table. Arguments: aliases (`~.utils.OrderByTuple`): optionally prefixed names of columns ('-' indicates descending order) in order of significance with regard to data ordering. """ modified_any = False accessors = [] for alias in aliases: bound_column = self.table.columns[OrderBy(alias).bare] # bound_column.order_by reflects the current ordering applied to # the table. As such we need to check the current ordering on the # column and use the opposite if it doesn't match the alias prefix. if alias[0] != bound_column.order_by_alias[0]: accessors += bound_column.order_by.opposite else: accessors += bound_column.order_by if bound_column: queryset, modified = bound_column.order(self.data, alias[0] == "-") if modified: self.data = queryset modified_any = True # custom ordering if modified_any: return # Traditional ordering if accessors: order_by_accessors = (a.for_queryset() for a in accessors) self.data = self.data.order_by(*order_by_accessors) @cached_property def verbose_name(self): """ The full (singular) name for the data. Model's `~django.db.Model.Meta.verbose_name` is honored. """ return self.data.model._meta.verbose_name @cached_property def verbose_name_plural(self): """ The full (plural) name for the data. Model's `~django.db.Model.Meta.verbose_name` is honored. """ return self.data.model._meta.verbose_name_plural django-tables2-2.1.1/django_tables2/templatetags/0000755000175000017500000000000013542204315021220 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/templatetags/django_tables2.py0000644000175000017500000002052313542204315024452 0ustar josephjosephimport re from collections import OrderedDict from django import template from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.template import Node, TemplateSyntaxError from django.template.loader import get_template, select_template from django.templatetags.l10n import register as l10n_register from django.utils.html import escape from django.utils.http import urlencode import django_tables2 as tables from django_tables2.utils import AttributeDict register = template.Library() kwarg_re = re.compile(r"(?:(.+)=)?(.+)") context_processor_error_msg = ( "Tag {%% %s %%} requires django.template.context_processors.request to be " "in the template configuration in " "settings.TEMPLATES[]OPTIONS.context_processors) in order for the included " "template tags to function correctly." ) def token_kwargs(bits, parser): """ Based on Django's `~django.template.defaulttags.token_kwargs`, but with a few changes: - No legacy mode. - Both keys and values are compiled as a filter """ if not bits: return {} kwargs = OrderedDict() while bits: match = kwarg_re.match(bits[0]) if not match or not match.group(1): return kwargs key, value = match.groups() del bits[:1] kwargs[parser.compile_filter(key)] = parser.compile_filter(value) return kwargs class QuerystringNode(Node): def __init__(self, updates, removals, asvar=None): super().__init__() self.updates = updates self.removals = removals self.asvar = asvar def render(self, context): if "request" not in context: raise ImproperlyConfigured(context_processor_error_msg % "querystring") params = dict(context["request"].GET) for key, value in self.updates.items(): if isinstance(key, str): params[key] = value continue key = key.resolve(context) value = value.resolve(context) if key not in ("", None): params[key] = value for removal in self.removals: params.pop(removal.resolve(context), None) value = escape("?" + urlencode(params, doseq=True)) if self.asvar: context[str(self.asvar)] = value return "" else: return value # {% querystring "name"="abc" "age"=15 as=qs %} @register.tag def querystring(parser, token): """ Creates a URL (containing only the query string [including "?"]) derived from the current URL's query string, by updating it with the provided keyword arguments. Example (imagine URL is ``/abc/?gender=male&name=Brad``):: # {% querystring "name"="abc" "age"=15 %} ?name=abc&gender=male&age=15 {% querystring "name"="Ayers" "age"=20 %} ?name=Ayers&gender=male&age=20 {% querystring "name"="Ayers" without "gender" %} ?name=Ayers """ bits = token.split_contents() tag = bits.pop(0) updates = token_kwargs(bits, parser) asvar_key = None for key in updates: if str(key) == "as": asvar_key = key if asvar_key is not None: asvar = updates[asvar_key] del updates[asvar_key] else: asvar = None # ``bits`` should now be empty of a=b pairs, it should either be empty, or # have ``without`` arguments. if bits and bits.pop(0) != "without": raise TemplateSyntaxError("Malformed arguments to '%s'" % tag) removals = [parser.compile_filter(bit) for bit in bits] return QuerystringNode(updates, removals, asvar=asvar) class RenderTableNode(Node): """ parameters: table (~.Table): the table to render template (str or list): Name[s] of template to render """ def __init__(self, table, template_name=None): super().__init__() self.table = table self.template_name = template_name def render(self, context): table = self.table.resolve(context) request = context.get("request") if isinstance(table, tables.TableBase): pass elif hasattr(table, "model"): queryset = table table = tables.table_factory(model=queryset.model)(queryset, request=request) else: klass = type(table).__name__ raise ValueError("Expected table or queryset, not {}".format(klass)) if self.template_name: template_name = self.template_name.resolve(context) else: template_name = table.template_name if isinstance(template_name, str): template = get_template(template_name) else: # assume some iterable was given template = select_template(template_name) try: # HACK: # TemplateColumn benefits from being able to use the context # that the table is rendered in. The current way this is # achieved is to temporarily attach the context to the table, # which TemplateColumn then looks for and uses. table.context = context table.before_render(request) return template.render(context={"table": table}, request=request) finally: del table.context @register.tag def render_table(parser, token): """ Render a HTML table. The tag can be given either a `.Table` object, or a queryset. An optional second argument can specify the template to use. Example:: {% render_table table %} {% render_table table "custom.html" %} {% render_table user_queryset %} When given a queryset, a `.Table` class is generated dynamically as follows:: class OnTheFlyTable(tables.Table): class Meta: model = queryset.model attrs = {'class': 'paleblue'} For configuration beyond this, a `.Table` class must be manually defined, instantiated, and passed to this tag. The context should include a *request* variable containing the current request. This allows pagination URLs to be created without clobbering the existing querystring. """ bits = token.split_contents() bits.pop(0) table = parser.compile_filter(bits.pop(0)) template = parser.compile_filter(bits.pop(0)) if bits else None return RenderTableNode(table, template) register.filter("localize", l10n_register.filters["localize"]) register.filter("unlocalize", l10n_register.filters["unlocalize"]) @register.simple_tag(takes_context=True) def export_url(context, export_format, export_trigger_param=None): """ Returns an export URL for the given file `export_format`, preserving current query string parameters. Example for a page requested with querystring ``?q=blue``:: {% export_url "csv" %} It will return:: ?q=blue&_export=csv """ if export_trigger_param is None and "view" in context: export_trigger_param = getattr(context["view"], "export_trigger_param", None) export_trigger_param = export_trigger_param or "_export" return QuerystringNode(updates={export_trigger_param: export_format}, removals=[]).render( context ) @register.filter def table_page_range(page, paginator): """ Given an page and paginator, return a list of max 10 (by default) page numbers: - always containing the first, last and current page. - containing one or two '...' to skip ranges between first/last and current. Example: {% for p in table.page|table_page_range:table.paginator %} {{ p }} {% endfor %} """ page_range = getattr(settings, "DJANGO_TABLES2_PAGE_RANGE", 10) num_pages = paginator.num_pages if num_pages <= page_range: return range(1, num_pages + 1) range_start = page.number - int(page_range / 2) if range_start < 1: range_start = 1 range_end = range_start + page_range if range_end > num_pages: range_start = num_pages - page_range + 1 range_end = num_pages + 1 ret = range(range_start, range_end) if 1 not in ret: ret = [1, "..."] + list(ret)[2:] if num_pages not in ret: ret = list(ret)[:-2] + ["...", num_pages] return ret @register.simple_tag def render_attrs(attrs, **kwargs): ret = AttributeDict(kwargs) if attrs is not None: ret.update(attrs) return ret.as_html() django-tables2-2.1.1/django_tables2/templatetags/__init__.py0000644000175000017500000000000013542204315023317 0ustar josephjosephdjango-tables2-2.1.1/django_tables2/static/0000755000175000017500000000000013542204315020015 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/static/django_tables2/0000755000175000017500000000000013542204315022673 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/static/django_tables2/themes/0000755000175000017500000000000013542204315024160 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/static/django_tables2/themes/paleblue/0000755000175000017500000000000013542204315025751 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/static/django_tables2/themes/paleblue/img/0000755000175000017500000000000013542204315026525 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/static/django_tables2/themes/paleblue/img/arrow-inactive-up.png0000644000175000017500000000032213542204315032604 0ustar josephjosephPNG  IHDR tEXtSoftwareAdobe ImageReadyqe<tIDATx| ( {Х`(1#ARDKg?PJi} P'd> 􅬵hIZ*+m9'@}UUVJA!=s5ˍ=Ӡ.>6WIENDB`django-tables2-2.1.1/django_tables2/static/django_tables2/themes/paleblue/img/pagination-bg.gif0000644000175000017500000000042113542204315031730 0ustar josephjosephGIF89a!, &dihlW,tmD|.(bal:tJ:ج zneL.ǐzNP|   !;django-tables2-2.1.1/django_tables2/static/django_tables2/themes/paleblue/img/false.gif0000644000175000017500000000026013542204315030304 0ustar josephjosephGIF89a !!33..HH``bbXXZZuurr!, - @4(D@:@Ю1A;p cM0ITa;django-tables2-2.1.1/django_tables2/static/django_tables2/themes/paleblue/img/missing.png0000644000175000017500000000077513542204315030715 0ustar josephjosephPNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATx|S=KA|`LU~`XDJ w(҆TB9,r zM,l-J4B7k^ff,㜳( ~OH]S}q֖"pOuL_W{ē'E>+D yq8:Y Amv So/Sm־Nsٜ/D4e*ڮaV'Vx|0PLk$wħ`e*Twxܟ22t:dx R #)vfVbc()Ȋ@ .F`"LIENDB`django-tables2-2.1.1/django_tables2/static/django_tables2/themes/paleblue/img/arrow-active-up.png0000644000175000017500000000031213542204315032254 0ustar josephjosephPNG  IHDR tEXtSoftwareAdobe ImageReadyqe<lIDATxbd@z-K5 Ç`q&tEn@A@@U!H'_0aAX@T?H###H/ @[ ` @=.E!cIENDB`django-tables2-2.1.1/django_tables2/static/django_tables2/themes/paleblue/img/header-bg.png0000644000175000017500000000020213542204315031043 0ustar josephjosephPNG  IHDRStEXtSoftwareAdobe ImageReadyqe<$IDATxb?t0NaM=z`> IENDB`django-tables2-2.1.1/django_tables2/static/django_tables2/themes/paleblue/css/0000755000175000017500000000000013542204315026541 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/static/django_tables2/themes/paleblue/css/screen.css0000644000175000017500000000532613542204315030540 0ustar josephjosephtable.paleblue { border-collapse: collapse; border-color: #CCC; border: 1px solid #DDD; } table.paleblue, table.paleblue + ul.pagination { font: normal 11px/14px 'Lucida Grande', Verdana, Helvetica, Arial, sans-serif; } table.paleblue a:link, table.paleblue a:visited, table.paleblue + ul.pagination > li > a { color: #5B80B2; text-decoration: none; font-weight: bold; } table.paleblue a:hover { color: #036; } table.paleblue td, table.paleblue th { padding: 5px; line-height: 13px; border-bottom: 1px solid #EEE; border-left: 1px solid #DDD; text-align: left; } table.paleblue thead th:first-child, table.paleblue thead td:first-child { border-left: none !important; } table.paleblue thead th, table.paleblue thead td { background: #FCFCFC url(../img/header-bg.png) left bottom repeat-x; border-bottom: 1px solid #DDD; padding: 2px 5px; font-size: 11px; vertical-align: middle; color: #666; } table.paleblue thead th > a:link, table.paleblue thead th > a:visited { color: #666; } table.paleblue thead th.orderable > a { padding-right: 20px; background: url(../img/arrow-inactive-up.png) right center no-repeat; } table.paleblue thead th.orderable.asc > a { background-image: url(../img/arrow-active-up.png); } table.paleblue thead th.orderable.desc > a { background-image: url(../img/arrow-active-down.png); } table.paleblue tr.odd { background-color: #EDF3FE; } table.paleblue tr.even { background-color: white; } table.paleblue + ul.pagination { background: white url(../img/pagination-bg.gif) left 180% repeat-x; overflow: auto; margin: 0; padding: 10px; border: 1px solid #DDD; list-style: none; } table.paleblue + ul.pagination > li { float: left; line-height: 22px; margin-left: 10px; } table.paleblue + ul.pagination > li:first-child { margin-left: 0; } table.paleblue + ul.pagination > li.cardinality { float: right; color: #8d8d8d; } table.paleblue > tbody > tr > td > span.true, table.paleblue > tbody > tr > td > span.false { background-position: top left; background-repeat: no-repeat; display: inline-block; height: 10px; overflow: hidden; text-indent: -200px; width: 10px; } table.paleblue > tbody > tr > td > .missing { background: transparent url(../img/missing.png) right center no-repeat; color: #717171; padding-right: 20px; } table.paleblue > tbody > tr > td > .missing:hover { color: #333; } table.paleblue > tbody > tr > td > span.true { background-image: url(../img/true.gif); } table.paleblue > tbody > tr > td > span.false { background-image: url(../img/false.gif); } div.table-container { display: inline-block; } django-tables2-2.1.1/django_tables2/static/django_tables2/bootstrap.css0000644000175000017500000000024713542204315025425 0ustar josephjoseph.table-container th.asc:after { content: '\0000a0\0025b2'; float: right; } .table-container th.desc:after { content: '\0000a0\0025bc'; float: right; } django-tables2-2.1.1/django_tables2/templates/0000755000175000017500000000000013542204315020524 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/templates/django_tables2/0000755000175000017500000000000013542204315023402 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/templates/django_tables2/table.html0000644000175000017500000001015113542204315025355 0ustar josephjoseph{% load django_tables2 %} {% load i18n %} {% block table-wrapper %}
{% block table %} {% block table.thead %} {% if table.show_header %} {% for column in table.columns %} {% endfor %} {% endif %} {% endblock table.thead %} {% block table.tbody %} {% for row in table.paginated_rows %} {% block table.tbody.row %} {% for column, cell in row.items %} {% endfor %} {% endblock table.tbody.row %} {% empty %} {% if table.empty_text %} {% block table.tbody.empty_text %} {% endblock table.tbody.empty_text %} {% endif %} {% endfor %} {% endblock table.tbody %} {% block table.tfoot %} {% if table.has_footer %} {% for column in table.columns %} {% endfor %} {% endif %} {% endblock table.tfoot %}
{% if column.orderable %} {{ column.header }} {% else %} {{ column.header }} {% endif %}
{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}
{{ table.empty_text }}
{{ column.footer }}
{% endblock table %} {% block pagination %} {% if table.page and table.paginator.num_pages > 1 %}
    {% if table.page.has_previous %} {% block pagination.previous %} {% endblock pagination.previous %} {% endif %} {% if table.page.has_previous or table.page.has_next %} {% block pagination.range %} {% for p in table.page|table_page_range:table.paginator %}
  • {% if p == '...' %} {{ p }} {% else %} {{ p }} {% endif %}
  • {% endfor %} {% endblock pagination.range %} {% endif %} {% if table.page.has_next %} {% block pagination.next %} {% endblock pagination.next %} {% endif %}
{% endif %} {% endblock pagination %}
{% endblock table-wrapper %} django-tables2-2.1.1/django_tables2/templates/django_tables2/bootstrap4.html0000644000175000017500000001024413542204315026372 0ustar josephjoseph{% load django_tables2 %} {% load i18n %} {% block table-wrapper %}
{% block table %} {% block table.thead %} {% if table.show_header %} {% for column in table.columns %} {% endfor %} {% endif %} {% endblock table.thead %} {% block table.tbody %} {% for row in table.paginated_rows %} {% block table.tbody.row %} {% for column, cell in row.items %} {% endfor %} {% endblock table.tbody.row %} {% empty %} {% if table.empty_text %} {% block table.tbody.empty_text %} {% endblock table.tbody.empty_text %} {% endif %} {% endfor %} {% endblock table.tbody %} {% block table.tfoot %} {% if table.has_footer %} {% for column in table.columns %} {% endfor %} {% endif %} {% endblock table.tfoot %}
{% if column.orderable %} {{ column.header }} {% else %} {{ column.header }} {% endif %}
{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}
{{ table.empty_text }}
{{ column.footer }}
{% endblock table %} {% block pagination %} {% if table.page and table.paginator.num_pages > 1 %} {% endif %} {% endblock pagination %}
{% endblock table-wrapper %} django-tables2-2.1.1/django_tables2/templates/django_tables2/semantic.html0000644000175000017500000001126413542204315026077 0ustar josephjoseph{% load django_tables2 %} {% load i18n %} {% block table-wrapper %}
{% block table %} {% block table.thead %} {% if table.show_header %} {% for column in table.columns %} {% endfor %} {% endif %} {% endblock table.thead %} {% block table.tbody %} {% for row in table.paginated_rows %} {% block table.tbody.row %} {% for column, cell in row.items %} {% endfor %} {% endblock table.tbody.row %} {% empty %} {% if table.empty_text %} {% block table.tbody.empty_text %} {% endblock table.tbody.empty_text %} {% endif %} {% endfor %} {% endblock table.tbody %} {% block table.tfoot %} {% if table.has_footer %} {% for column in table.columns %} {% endfor %} {% endif %} {% block pagination %} {% if table.page and table.paginator.num_pages > 1 %} {% endif %} {% endblock pagination %} {% endblock table.tfoot %}
{% if column.orderable %} {{ column.header }} {% else %} {{ column.header }} {% endif %}
{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}
{{ table.empty_text }}
{{ column.footer }}
{% endblock table %}
{% endblock table-wrapper %} django-tables2-2.1.1/django_tables2/templates/django_tables2/bootstrap-responsive.html0000644000175000017500000000057313542204315030505 0ustar josephjoseph{% extends 'django_tables2/bootstrap.html' %} {% block table-wrapper %}
{% block table %} {{ block.super }} {% endblock table %} {% block pagination %} {% if table.page and table.paginator.num_pages > 1 %} {{ block.super }} {% endif %} {% endblock pagination %}
{% endblock table-wrapper %} django-tables2-2.1.1/django_tables2/templates/django_tables2/bootstrap.html0000644000175000017500000001057713542204315026317 0ustar josephjoseph{% load django_tables2 %} {% load i18n %} {% block table-wrapper %}
{% block table %} {% block table.thead %} {% if table.show_header %} {% for column in table.columns %} {% endfor %} {% endif %} {% endblock table.thead %} {% block table.tbody %} {% for row in table.paginated_rows %} {% block table.tbody.row %} {% for column, cell in row.items %} {% endfor %} {% endblock table.tbody.row %} {% empty %} {% if table.empty_text %} {% block table.tbody.empty_text %} {% endblock table.tbody.empty_text %} {% endif %} {% endfor %} {% endblock table.tbody %} {% block table.tfoot %} {% if table.has_footer %} {% for column in table.columns %} {% endfor %} {% endif %} {% endblock table.tfoot %}
{% if column.orderable %} {{ column.header }} {% else %} {{ column.header }} {% endif %}
{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}
{{ table.empty_text }}
{{ column.footer }}
{% endblock table %} {% block pagination %} {% if table.page and table.paginator.num_pages > 1 %} {% endif %} {% endblock pagination %}
{% endblock table-wrapper %} django-tables2-2.1.1/django_tables2/rows.py0000644000175000017500000002607213542204315020101 0ustar josephjosephfrom django.db import models from django.db.models.fields import FieldDoesNotExist from .columns.linkcolumn import BaseLinkColumn from .utils import A, AttributeDict, call_with_appropriate, computed_values class CellAccessor: """ Allows accessing cell contents on a row object (see `BoundRow`) """ def __init__(self, row): self.row = row def __getitem__(self, key): return self.row.get_cell(key) def __getattr__(self, name): return self.row.get_cell(name) class BoundRow: """ Represents a *specific* row in a table. `.BoundRow` objects are a container that make it easy to access the final 'rendered' values for cells in a row. You can simply iterate over a `.BoundRow` object and it will take care to return values rendered using the correct method (e.g. :ref:`table.render_FOO`) To access the rendered value of each cell in a row, just iterate over it:: >>> import django_tables2 as tables >>> class SimpleTable(tables.Table): ... a = tables.Column() ... b = tables.CheckBoxColumn(attrs={'name': 'my_chkbox'}) ... >>> table = SimpleTable([{'a': 1, 'b': 2}]) >>> row = table.rows[0] # we only have one row, so let's use it >>> for cell in row: ... print(cell) ... 1 Alternatively you can use row.cells[0] to retrieve a specific cell:: >>> row.cells[0] 1 >>> row.cells[1] '' >>> row.cells[2] ... IndexError: list index out of range Finally you can also use the column names to retrieve a specific cell:: >>> row.cells.a 1 >>> row.cells.b '' >>> row.cells.c ... KeyError: "Column with name 'c' does not exist; choices are: ['a', 'b']" If you have the column name in a variable, you can also treat the `cells` property like a `dict`:: >>> key = 'a' >>> row.cells[key] 1 Arguments: table: The `.Table` in which this row exists. record: a single record from the :term:`table data` that is used to populate the row. A record could be a `~django.db.Model` object, a `dict`, or something else. """ def __init__(self, record, table): self._record = record self._table = table self.row_counter = next(table._counter) # support accessing cells from a template: {{ row.cells.column_name }} self.cells = CellAccessor(self) @property def table(self): """ The associated `.Table` object. """ return self._table def get_even_odd_css_class(self): """ Return css class, alternating for odd and even records. Return: string: `even` for even records, `odd` otherwise. """ return "odd" if self.row_counter % 2 else "even" @property def attrs(self): """ Return the attributes for a certain row. """ cssClass = self.get_even_odd_css_class() row_attrs = computed_values( self._table.row_attrs, kwargs=dict(table=self._table, record=self._record) ) if "class" in row_attrs and row_attrs["class"]: row_attrs["class"] += " " + cssClass else: row_attrs["class"] = cssClass return AttributeDict(row_attrs) @property def record(self): """ The data record from the data source which is used to populate this row with data. """ return self._record def __iter__(self): """ Iterate over the rendered values for cells in the row. Under the hood this method just makes a call to `.BoundRow.__getitem__` for each cell. """ for column, value in self.items(): # this uses __getitem__, using the name (rather than the accessor) # is correct – it's what __getitem__ expects. yield value def _get_and_render_with(self, bound_column, render_func, default): value = None accessor = A(bound_column.accessor) # We need to take special care here to allow get_FOO_display() # methods on a model to be used if available. See issue #30. penultimate, remainder = accessor.penultimate(self.record) # If the penultimate is a model and the remainder is a field # using choices, use get_FOO_display(). if isinstance(penultimate, models.Model): try: field = accessor.get_field(self.record) display_fn = getattr(penultimate, "get_%s_display" % remainder, None) if getattr(field, "choices", ()) and display_fn: value = display_fn() remainder = None except FieldDoesNotExist: pass # Fall back to just using the original accessor if remainder: try: value = accessor.resolve(self.record) except Exception: # we need to account for non-field based columns (issue #257) is_linkcolumn = isinstance(bound_column.column, BaseLinkColumn) if is_linkcolumn and bound_column.column.text is not None: return render_func(bound_column) if value in bound_column.column.empty_values: return default return render_func(bound_column, value) def _optional_cell_arguments(self, bound_column, value): """ Defines the arguments that will optionally be passed while calling the cell's rendering or value getter if that function has one of these as a keyword argument. """ return { "value": value, "record": self.record, "column": bound_column.column, "bound_column": bound_column, "bound_row": self, "table": self._table, } def get_cell(self, name): """ Returns the final rendered html for a cell in the row, given the name of a column. """ bound_column = self.table.columns[name] return self._get_and_render_with( bound_column, render_func=self._call_render, default=bound_column.default ) def _call_render(self, bound_column, value=None): """ Call the column's render method with appropriate kwargs """ render_kwargs = self._optional_cell_arguments(bound_column, value) content = call_with_appropriate(bound_column.render, render_kwargs) return bound_column.link(content, **render_kwargs) if bound_column.link else content def get_cell_value(self, name): """ Returns the final rendered value (excluding any html) for a cell in the row, given the name of a column. """ return self._get_and_render_with( self.table.columns[name], render_func=self._call_value, default=None ) def _call_value(self, bound_column, value=None): """ Call the column's value method with appropriate kwargs """ return call_with_appropriate( bound_column.value, self._optional_cell_arguments(bound_column, value) ) def __contains__(self, item): """ Check by both row object and column name. """ return item in (self.table.columns if isinstance(item, str) else self) def items(self): """ Returns iterator yielding ``(bound_column, cell)`` pairs. *cell* is ``row[name]`` -- the rendered unicode value that should be ``rendered within ``
``. """ for column in self.table.columns: # column gets some attributes relevant only relevant in this iteration, # used to allow passing the value/record to a callable Column.attrs / # Table.attrs item. column.current_value = self.get_cell(column.name) column.current_record = self.record yield (column, column.current_value) class BoundPinnedRow(BoundRow): """ Represents a *pinned* row in a table. """ @property def attrs(self): """ Return the attributes for a certain pinned row. Add CSS classes `pinned-row` and `odd` or `even` to `class` attribute. Return: AttributeDict: Attributes for pinned rows. """ row_attrs = computed_values(self._table.pinned_row_attrs, kwargs={"record": self._record}) css_class = " ".join( [self.get_even_odd_css_class(), "pinned-row", row_attrs.get("class", "")] ) row_attrs["class"] = css_class return AttributeDict(row_attrs) class BoundRows: """ Container for spawning `.BoundRow` objects. Arguments: data: iterable of records table: the `~.Table` in which the rows exist pinned_data: dictionary with iterable of records for top and/or bottom pinned rows. Example: >>> pinned_data = { ... 'top': iterable, # or None value ... 'bottom': iterable, # or None value ... } This is used for `~.Table.rows`. """ def __init__(self, data, table, pinned_data=None): self.data = data self.table = table self.pinned_data = pinned_data or {} def generator_pinned_row(self, data): """ Top and bottom pinned rows generator. Arguments: data: Iterable data for all records for top or bottom pinned rows. Yields: BoundPinnedRow: Top or bottom `BoundPinnedRow` object for single pinned record. """ if data is not None: if hasattr(data, "__iter__") is False: raise ValueError("The data for pinned rows must be iterable") for pinned_record in data: yield BoundPinnedRow(pinned_record, table=self.table) def __iter__(self): # Top pinned rows for pinned_record in self.generator_pinned_row(self.pinned_data.get("top")): yield pinned_record for record in self.data: yield BoundRow(record, table=self.table) # Bottom pinned rows for pinned_record in self.generator_pinned_row(self.pinned_data.get("bottom")): yield pinned_record def __len__(self): length = len(self.data) pinned_top = self.pinned_data.get("top") pinned_bottom = self.pinned_data.get("bottom") length += 0 if pinned_top is None else len(pinned_top) length += 0 if pinned_bottom is None else len(pinned_bottom) return length def __getitem__(self, key): """ Slicing returns a new `~.BoundRows` instance, indexing returns a single `~.BoundRow` instance. """ if isinstance(key, slice): return BoundRows(data=self.data[key], table=self.table, pinned_data=self.pinned_data) else: return BoundRow(record=self.data[key], table=self.table) django-tables2-2.1.1/django_tables2/export/0000755000175000017500000000000013542204315020047 5ustar josephjosephdjango-tables2-2.1.1/django_tables2/export/views.py0000644000175000017500000000411613542204315021560 0ustar josephjosephfrom .export import TableExport class ExportMixin: """ Support various export formats for the table data. `ExportMixin` looks for some attributes on the class to change it's behavior: Attributes: export_class (TableExport): Allows using a custom implementation of `TableExport`. export_name (str): is the name of file that will be exported, without extension. export_trigger_param (str): is the name of the GET attribute used to trigger the export. It's value decides the export format, refer to `TableExport` for a list of available formats. exclude_columns (iterable): column names excluded from the export. For example, one might want to exclude columns containing buttons from the export. Excluding columns from the export is also possible using the `exclude_from_export` argument to the `.Column` constructor:: class Table(tables.Table): name = tables.Column() buttons = tables.TemplateColumn(exclude_from_export=True, template_name=...) export_formats (iterable): export formats to render a set of buttons in the template. """ export_class = TableExport export_name = "table" export_trigger_param = "_export" exclude_columns = () export_formats = (TableExport.CSV,) def get_export_filename(self, export_format): return "{}.{}".format(self.export_name, export_format) def create_export(self, export_format): exporter = self.export_class( export_format=export_format, table=self.get_table(**self.get_table_kwargs()), exclude_columns=self.exclude_columns, ) return exporter.response(filename=self.get_export_filename(export_format)) def render_to_response(self, context, **kwargs): export_format = self.request.GET.get(self.export_trigger_param, None) if self.export_class.is_valid_format(export_format): return self.create_export(export_format) return super().render_to_response(context, **kwargs) django-tables2-2.1.1/django_tables2/export/__init__.py0000644000175000017500000000015113542204315022155 0ustar josephjosephfrom .export import TableExport from .views import ExportMixin __all__ = ("TableExport", "ExportMixin") django-tables2-2.1.1/django_tables2/export/export.py0000644000175000017500000000557613542204315021757 0ustar josephjosephfrom django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse try: from tablib import Dataset except ImportError: # pragma: no cover raise ImproperlyConfigured( "You must have tablib installed in order to use the django-tables2 export functionality" ) class TableExport: """ Export data from a table to the file type specified. Arguments: export_format (str): one of `csv, json, latex, ods, tsv, xls, xlsx, yml` table (`~.Table`): instance of the table to export the data from exclude_columns (iterable): list of column names to exclude from the export """ CSV = "csv" JSON = "json" LATEX = "latex" ODS = "ods" TSV = "tsv" XLS = "xls" XLSX = "xlsx" YAML = "yml" FORMATS = { CSV: "text/csv; charset=utf-8", JSON: "application/json", LATEX: "text/plain", ODS: "application/vnd.oasis.opendocument.spreadsheet", TSV: "text/tsv; charset=utf-8", XLS: "application/vnd.ms-excel", XLSX: "application/vnd.ms-excel", YAML: "text/yml; charset=utf-8", } def __init__(self, export_format, table, exclude_columns=None): if not self.is_valid_format(export_format): raise TypeError('Export format "{}" is not supported.'.format(export_format)) self.format = export_format self.dataset = self.table_to_dataset(table, exclude_columns) def table_to_dataset(self, table, exclude_columns): """Transform a table to a tablib dataset.""" dataset = Dataset() for i, row in enumerate(table.as_values(exclude_columns=exclude_columns)): if i == 0: dataset.headers = row else: dataset.append(row) return dataset @classmethod def is_valid_format(self, export_format): """ Returns true if `export_format` is one of the supported export formats """ return export_format is not None and export_format in TableExport.FORMATS.keys() def content_type(self): """ Returns the content type for the current export format """ return self.FORMATS[self.format] def export(self): """ Returns the string/bytes for the current export format """ return getattr(self.dataset, self.format) def response(self, filename=None): """ Builds and returns a `HttpResponse` containing the exported data Arguments: filename (str): if not `None`, the filename is attached to the `Content-Disposition` header of the response. """ response = HttpResponse(content_type=self.content_type()) if filename is not None: response["Content-Disposition"] = 'attachment; filename="{}"'.format(filename) response.write(self.export()) return response