Skip to content

Table Definition

The table definition holds the configuration for a table, including its columns, data source, and other options.

Below you can check the available attributes of the TableDefinition class and their descriptions. Also, a full code is provided at the end of this document for reference.

Table definition.

ATTRIBUTE DESCRIPTION
name

Unique identifier for the table.

TYPE: str

data_source

Data source for the table. Mix in :class:~ckanext.tables.cache.CachedDataSourceMixin on the data source to enable caching; the TTL and backend are configured there.

TYPE: BaseDataSource

ajax_url

(Optional) URL to fetch data from. Defaults to an auto-generated URL.

TYPE: str | None

columns

(Optional) List of ColumnDefinition objects.

TYPE: list[ColumnDefinition]

row_actions

(Optional) List of RowActionDefinition objects.

TYPE: list[RowActionDefinition]

bulk_actions

(Optional) List of BulkActionDefinition objects for action on multiple rows.

TYPE: list[BulkActionDefinition]

table_actions

(Optional) List of TableActionDefinition objects for actions on the table itself.

TYPE: list[TableActionDefinition]

exporters

(Optional) List of exporter classes for exporting table data.

TYPE: list[type[ExporterBase]]

placeholder

(Optional) Placeholder text for an empty table.

TYPE: str | None

page_size

(Optional) Number of rows per page. Defaults to 10.

TYPE: int

table_template

(Optional) Template to render the table. Defaults to tables/base.html.

TYPE: str

Source code in ckanext/tables/table.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
@dataclass
class TableDefinition:
    """Table definition.

    Attributes:
        name: Unique identifier for the table.
        data_source: Data source for the table. Mix in
            :class:`~ckanext.tables.cache.CachedDataSourceMixin` on the data
            source to enable caching; the TTL and backend are configured there.
        ajax_url: (Optional) URL to fetch data from. Defaults to an auto-generated URL.
        columns: (Optional) List of ColumnDefinition objects.
        row_actions: (Optional) List of RowActionDefinition objects.
        bulk_actions: (Optional) List of BulkActionDefinition objects for action on multiple rows.
        table_actions: (Optional) List of TableActionDefinition objects for actions on the table itself.
        exporters: (Optional) List of exporter classes for exporting table data.
        placeholder: (Optional) Placeholder text for an empty table.
        page_size: (Optional) Number of rows per page. Defaults to 10.
        table_template: (Optional) Template to render the table. Defaults to `tables/base.html`.
    """

    name: str
    data_source: BaseDataSource
    ajax_url: str | None = None
    columns: list[ColumnDefinition] = dataclass_field(default_factory=list)
    row_actions: list[RowActionDefinition] = dataclass_field(default_factory=list)
    bulk_actions: list[BulkActionDefinition] = dataclass_field(default_factory=list)
    table_actions: list[TableActionDefinition] = dataclass_field(default_factory=list)
    exporters: list[type[ExporterBase]] = dataclass_field(default_factory=list)
    placeholder: str | None = None
    page_size: int = 10
    table_template: str = "tables/base.html"

    def __post_init__(self):
        # Wire up caching through the data source's backend, if it has one.
        if isinstance(self.data_source, CachedDataSourceMixin):
            self._cache = self.data_source.cache_backend
            self._cache_key = f"table:{self.name}"
            self._cache_ttl = self.data_source.cache_ttl
        else:
            self._cache = None
            self._cache_key = ""
            self._cache_ttl = 0

        self.id = f"table_{self.name}_{uuid.uuid4().hex[:8]}"

        if self.placeholder is None:
            self.placeholder = tk._("No data found")

        if self.row_actions:
            self.columns.append(
                ColumnDefinition(
                    field=COLUMN_ACTIONS_FIELD,
                    title=tk._(""),
                    formatters=[(formatters.ActionsFormatter, {})],
                    filterable=False,
                    tabulator_formatter="html",
                    sortable=False,
                    resizable=False,
                    width=50,
                ),
            )

    def get_tabulator_config(self) -> dict[str, Any]:
        columns = [col.to_dict() for col in self.columns]

        options: dict[str, Any] = {
            "columns": columns,
            "placeholder": self.placeholder,
            "sortMode": "remote",
            "layout": "fitColumns",
            "pagination": True,
            "paginationMode": "remote",
            "paginationSize": self.page_size,
            "paginationSizeSelector": [5, 10, 25, 50, 100],
            "minHeight": 300,
            "filterMode": "remote",
        }

        if self.ajax_url:
            options["ajaxURL"] = self.ajax_url

        if bool(self.bulk_actions):
            options.update(
                {
                    "rowHeader": {
                        "headerSort": False,
                        "resizable": False,
                        "headerHozAlign": "center",
                        "hozAlign": "center",
                        "vertAlign": "middle",
                        "formatter": "rowSelection",
                        "titleFormatter": "rowSelection",
                        "width": 48,
                        "height": 48,
                    }
                }
            )

        return options

    def get_row_actions(self) -> dict[str, dict[str, Any]]:
        return {
            action.action: {
                "name": action.action,
                "label": action.label,
                "icon": action.icon,
                "attrs": action.attrs,
                "with_confirmation": action.with_confirmation,
            }
            for action in self.row_actions
        }

    def render_table(self, **kwargs: Any) -> str:
        return tk.render(self.table_template, extra_vars={"table": self, **kwargs})

    def get_data(self, params: types.QueryParams) -> list[Any]:
        return [self._apply_formatters(dict(row)) for row in self.get_raw_data(params)]

    def get_raw_data(self, params: types.QueryParams, paginate: bool = True) -> list[dict[str, Any]]:
        if not paginate:
            return self.data_source.filter(params.filters).sort(params.sort_by, params.sort_order).all()

        return (
            self.data_source.filter(params.filters)
            .sort(params.sort_by, params.sort_order)
            .paginate(params.page, params.size)
            .all()
        )

    def get_total_count(self, params: types.QueryParams) -> int:
        cached = self._get_cached_count(params)

        if cached is not None:
            return cached

        # for total count we only apply filter, without sort and pagination
        count = self.data_source.filter(params.filters).count()

        self._set_cached_count(params, count)

        return count

    def _count_cache_key(self, params: types.QueryParams) -> str:
        """Return the cache sub-key for a given set of query params."""
        return f"{self._cache_key}:count:{params!s}" if params.filters else f"{self._cache_key}:count"

    def _get_cached_count(self, params: types.QueryParams) -> int | None:
        if self._cache is None:
            return None

        result = self._cache.get(self._count_cache_key(params))

        if result is not None:
            return int(result)

        return None

    def _set_cached_count(self, params: types.QueryParams, count: int) -> None:
        if self._cache is None:
            return

        self._cache.set(self._count_cache_key(params), count, self._cache_ttl)

    def _apply_formatters(self, row: dict[str, Any]) -> dict[str, Any]:
        """Apply formatters to each cell in a row."""
        formatted_row = copy.deepcopy(row)

        for column in self.columns:
            cell_value = row.get(column.field)

            if not column.formatters:
                continue

            for formatter_class, formatter_options in column.formatters:
                cell_value = formatter_class(column, formatted_row, row, self).format(cell_value, formatter_options)

            formatted_row[column.field] = cell_value

        return formatted_row

    @classmethod
    def check_access(cls, context: Context) -> None:
        """Check if the current user has access to view the table.

        This class method can be overridden in subclasses to implement
        custom access control logic.

        By default, it checks if the user has the `sysadmin` permission,
        which means that the table is available only to system administrators.

        Raises:
            tk.NotAuthorized: If the user does not have an access
        """
        tk.check_access("sysadmin", context)

    def get_bulk_action(self, action: str) -> BulkActionDefinition | None:
        return next((a for a in self.bulk_actions if a.action == action), None)

    def get_table_action(self, action: str) -> TableActionDefinition | None:
        return next((a for a in self.table_actions if a.action == action), None)

    def get_row_action(self, action: str) -> RowActionDefinition | None:
        return next((a for a in self.row_actions if a.action == action), None)

    def get_exporter(self, name: str) -> type[ExporterBase] | None:
        return next((e for e in self.exporters if e.name == name), None)

    def refresh_data(self) -> None:
        if self._cache is not None:
            self._cache.delete(self._cache_key)
            # Also clear any cached counts
            self._cache.delete(f"{self._cache_key}:count")

check_access(context) classmethod

Check if the current user has access to view the table.

This class method can be overridden in subclasses to implement custom access control logic.

By default, it checks if the user has the sysadmin permission, which means that the table is available only to system administrators.

RAISES DESCRIPTION
NotAuthorized

If the user does not have an access

Source code in ckanext/tables/table.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
@classmethod
def check_access(cls, context: Context) -> None:
    """Check if the current user has access to view the table.

    This class method can be overridden in subclasses to implement
    custom access control logic.

    By default, it checks if the user has the `sysadmin` permission,
    which means that the table is available only to system administrators.

    Raises:
        tk.NotAuthorized: If the user does not have an access
    """
    tk.check_access("sysadmin", context)