Skip to content

Commit

Permalink
New sortable_columns option in metadata.json to control sort options
Browse files Browse the repository at this point in the history
You can now explicitly set which columns in a table can be used for sorting
using the _sort and _sort_desc arguments using metadata.json:

    {
        "databases": {
            "database1": {
                "tables": {
                    "example_table": {
                        "sortable_columns": [
                            "height",
                            "weight"
                        ]
                    }
                }
            }
        }
    }

Refs #189
  • Loading branch information
simonw authored and Simon Willison committed Apr 9, 2018
1 parent a87df96 commit b13f098
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 23 deletions.
32 changes: 27 additions & 5 deletions datasette/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,10 +456,27 @@ async def view_get(self, request, name, hash, **kwargs):


class RowTableShared(BaseView):
def sortable_columns_for_table(self, name, table, use_rowid):
table_metadata = self.ds.metadata.get(
'databases', {}
).get(name, {}).get('tables', {}).get(table, {})
if 'sortable_columns' in table_metadata:
sortable_columns = set(table_metadata['sortable_columns'])
else:
table_info = self.ds.inspect()[name]['tables'].get(table) or {}
sortable_columns = set(table_info.get('columns', []))
if use_rowid:
sortable_columns.add('rowid')
return sortable_columns

async def display_columns_and_rows(self, database, table, description, rows, link_column=False, expand_foreign_keys=True):
"Returns columns, rows for specified table - including fancy foreign key treatment"
info = self.ds.inspect()[database]
columns = [r[0] for r in description]
sortable_columns = self.sortable_columns_for_table(database, table, True)
columns = [{
'name': r[0],
'sortable': r[0] in sortable_columns,
} for r in description]
tables = info['tables']
table_info = tables.get(table) or {}
pks = await self.pks_for_table(database, table)
Expand Down Expand Up @@ -505,7 +522,8 @@ async def display_columns_and_rows(self, database, table, description, rows, lin
)
),
})
for value, column in zip(row, columns):
for value, column_dict in zip(row, columns):
column = column_dict['name']
if (column, value) in expanded:
other_table, label = expanded[(column, value)]
display_value = jinja2.Markup(
Expand Down Expand Up @@ -533,7 +551,10 @@ async def display_columns_and_rows(self, database, table, description, rows, lin
cell_rows.append(cells)

if link_column:
columns = ['Link'] + columns
columns = [{
'name': 'Link',
'sortable': False,
}] + columns
return columns, cell_rows


Expand Down Expand Up @@ -623,7 +644,7 @@ async def data(self, request, name, hash, table):
if not is_view:
table_info = info[name]['tables'][table]
table_rows = table_info['count']
sortable_columns = set(table_info['columns'])
sortable_columns = self.sortable_columns_for_table(name, table, use_rowid)

# Allow for custom sort order
sort = special_args.get('_sort')
Expand Down Expand Up @@ -885,6 +906,8 @@ async def template_data():
display_columns, display_rows = await self.display_columns_and_rows(
name, table, description, rows, link_column=False, expand_foreign_keys=True
)
for column in display_columns:
column['sortable'] = False
return {
'database_hash': hash,
'foreign_key_tables': await self.foreign_key_tables(name, table, pk_values),
Expand All @@ -895,7 +918,6 @@ async def template_data():
'_rows_and_columns-row-{}-{}.html'.format(to_css_class(name), to_css_class(table)),
'_rows_and_columns.html',
],
'disable_sort': True,
'enumerate': enumerate,
'metadata': self.ds.metadata.get(
'databases', {}
Expand Down
12 changes: 6 additions & 6 deletions datasette/templates/_rows_and_columns.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<table>
<thead>
<tr>
{% for i, column in enumerate(display_columns) %}
{% for column in display_columns %}
<th scope="col">
{% if i == 0 or disable_sort %}
{{ column }}
{% if not column.sortable %}
{{ column.name }}
{% else %}
{% if column == sort %}
<a href="{{ path_with_added_args(request, {'_sort_desc': column, '_sort': None, '_next': None}) }}" rel="nofollow">{{ column }}&nbsp;▼</a>
{% if column.name == sort %}
<a href="{{ path_with_added_args(request, {'_sort_desc': column.name, '_sort': None, '_next': None}) }}" rel="nofollow">{{ column.name }}&nbsp;▼</a>
{% else %}
<a href="{{ path_with_added_args(request, {'_sort': column, '_sort_desc': None, '_next': None}) }}" rel="nofollow">{{ column }}{% if column == sort_desc %}&nbsp;▲{% endif %}</a>
<a href="{{ path_with_added_args(request, {'_sort': column.name, '_sort_desc': None, '_next': None}) }}" rel="nofollow">{{ column.name }}{% if column.name == sort_desc %}&nbsp;▲{% endif %}</a>
{% endif %}
{% endif %}
</th>
Expand Down
2 changes: 1 addition & 1 deletion datasette/templates/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<style>
@media only screen and (max-width: 576px) {
{% for column in display_columns %}
td:nth-of-type({{ loop.index }}):before { content: "{{ column|escape_css_string }}"; }
td:nth-of-type({{ loop.index }}):before { content: "{{ column.name|escape_css_string }}"; }
{% endfor %}
}
</style>
Expand Down
27 changes: 27 additions & 0 deletions docs/metadata.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,33 @@ You can also provide metadata at the per-database or per-table level, like this:

Each of the top-level metadata fields can be used at the database and table level.

Setting which columns can be used for sorting
---------------------------------------------

Datasette allows any column to be used for sorting by default. If you need to
control which columns are available for sorting you can do so using the optional
``sortable_columns`` key::

{
"databases": {
"database1": {
"tables": {
"example_table": {
"sortable_columns": [
"height",
"weight"
]
}
}
}
}
}

This will restrict sorting of ``example_table`` to just the ``height`` and
``weight`` columns.

You can also disable sorting entirely by setting ``"sortable_columns": []``

Generating a metadata skeleton
------------------------------

Expand Down
14 changes: 12 additions & 2 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,19 @@ def generate_sortable_rows(num):
'simple_primary_key': {
'description_html': 'Simple <em>primary</em> key',
'title': 'This <em>HTML</em> is escaped',
}
},
'sortable': {
'sortable_columns': [
'sortable',
'sortable_with_nulls',
'sortable_with_nulls_2',
]
},
'no_primary_key': {
'sortable_columns': [],
},
}
}
},
}
}

Expand Down
17 changes: 16 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,12 +412,27 @@ def test_sortable_argument_errors(app_client):
)
assert 'Cannot sort table by badcolumn2' == response.json['error']
response = app_client.get(
'/test_tables/sortable.json?_sort=content&_sort_desc=pk2',
'/test_tables/sortable.json?_sort=sortable_with_nulls&_sort_desc=sortable',
gather_request=False
)
assert 'Cannot use _sort and _sort_desc at the same time' == response.json['error']


def test_sortable_columns_metadata(app_client):
response = app_client.get(
'/test_tables/sortable.json?_sort=content',
gather_request=False
)
assert 'Cannot sort table by content' == response.json['error']
# no_primary_key has ALL sort options disabled
for column in ('content', 'a', 'b', 'c'):
response = app_client.get(
'/test_tables/sortable.json?_sort={}'.format(column),
gather_request=False
)
assert 'Cannot sort table by {}'.format(column) == response.json['error']


@pytest.mark.parametrize('path,expected_rows', [
('/test_tables/simple_primary_key.json?content=hello', [
['1', 'hello'],
Expand Down
12 changes: 4 additions & 8 deletions tests/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,10 @@ def test_row_html_simple_primary_key(app_client):
def test_table_html_no_primary_key(app_client):
response = app_client.get('/test_tables/no_primary_key', gather_request=False)
table = Soup(response.body, 'html.parser').find('table')
ths = table.findAll('th')
assert 'Link' == ths[0].string.strip()
for expected_col, th in zip(('rowid', 'content', 'a', 'b', 'c'), ths[1:]):
a = th.find('a')
assert expected_col == a.string
assert a['href'].endswith('/no_primary_key?_sort={}'.format(
expected_col
))
# We have disabled sorting for this table using metadata.json
assert [
'content', 'a', 'b', 'c'
] == [th.string.strip() for th in table.select('thead th')[2:]]
expected = [
[
'<td><a href="/test_tables/no_primary_key/{}">{}</a></td>'.format(i, i),
Expand Down

0 comments on commit b13f098

Please sign in to comment.