mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Compare commits
5 commits
main
...
filter-plu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00c5637e2b |
||
|
|
6bfc1507ff |
||
|
|
61f51d9ae6 |
||
|
|
5c02a6b0f4 |
||
|
|
5116c4ec8a |
9 changed files with 134 additions and 26 deletions
|
|
@ -33,3 +33,8 @@ def publish_subcommand(publish):
|
|||
@hookspec(firstresult=True)
|
||||
def render_cell(value):
|
||||
"Customize rendering of HTML table cell values"
|
||||
|
||||
|
||||
@hookspec
|
||||
def table_filter():
|
||||
"Custom filtering of the current table based on the request"
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
{% if query.sql %}
|
||||
{% if query.sql and config.allow_sql %}
|
||||
<p><a class="not-underlined" title="{{ query.sql }}" href="/{{ database }}-{{ database_hash }}?{{ {'sql': query.sql}|urlencode|safe }}{% if query.params %}&{{ query.params|urlencode|safe }}{% endif %}">✎ <span class="underlined">View and edit SQL</span></a></p>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from contextlib import contextmanager
|
||||
from collections import OrderedDict
|
||||
from collections import OrderedDict, namedtuple
|
||||
import base64
|
||||
import click
|
||||
import hashlib
|
||||
|
|
@ -39,6 +39,10 @@ RUN apt-get update && \
|
|||
ENV SQLITE_EXTENSIONS /usr/lib/x86_64-linux-gnu/mod_spatialite.so
|
||||
'''
|
||||
|
||||
TableFilter = namedtuple("TableFilter", (
|
||||
"human_description_extras", "where_clauses", "params")
|
||||
)
|
||||
|
||||
|
||||
class InterruptedError(Exception):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -309,22 +309,26 @@ class TableView(RowTableShared):
|
|||
where_clauses, params = filters.build_where_clauses()
|
||||
|
||||
# _search support:
|
||||
fts_table = info[name]["tables"].get(table, {}).get("fts_table")
|
||||
fts_table = table_metadata.get(
|
||||
"fts_table", info[name]["tables"].get(table, {}).get("fts_table")
|
||||
)
|
||||
fts_pk = table_metadata.get("fts_pk", "rowid")
|
||||
search_args = dict(
|
||||
pair for pair in special_args.items() if pair[0].startswith("_search")
|
||||
)
|
||||
search_descriptions = []
|
||||
human_description_extras = []
|
||||
search = ""
|
||||
if fts_table and search_args:
|
||||
if "_search" in search_args:
|
||||
# Simple ?_search=xxx
|
||||
search = search_args["_search"]
|
||||
where_clauses.append(
|
||||
"rowid in (select rowid from {fts_table} where {fts_table} match :search)".format(
|
||||
"{fts_pk} in (select rowid from {fts_table} where {fts_table} match :search)".format(
|
||||
fts_table=escape_sqlite(fts_table),
|
||||
fts_pk=escape_sqlite(fts_pk)
|
||||
)
|
||||
)
|
||||
search_descriptions.append('search matches "{}"'.format(search))
|
||||
human_description_extras.append('search matches "{}"'.format(search))
|
||||
params["search"] = search
|
||||
else:
|
||||
# More complex: search against specific columns
|
||||
|
|
@ -341,18 +345,32 @@ class TableView(RowTableShared):
|
|||
i=i
|
||||
)
|
||||
)
|
||||
search_descriptions.append(
|
||||
human_description_extras.append(
|
||||
'search column "{}" matches "{}"'.format(
|
||||
search_col, search_text
|
||||
)
|
||||
)
|
||||
params["search_{}".format(i)] = search_text
|
||||
|
||||
# filter_arguments plugin hook support
|
||||
for awaitable_fn in pm.hook.table_filter():
|
||||
if awaitable_fn is None:
|
||||
continue
|
||||
extras = await awaitable_fn(
|
||||
view=self, name=name, table=table, request=request
|
||||
)
|
||||
if extras is None:
|
||||
continue
|
||||
human_description_extras.extend(extras.human_description_extras)
|
||||
where_clauses.extend(extras.where_clauses)
|
||||
params.update(extras.params)
|
||||
|
||||
table_rows_count = None
|
||||
sortable_columns = set()
|
||||
if not is_view:
|
||||
table_rows_count = table_info["count"]
|
||||
sortable_columns = self.sortable_columns_for_table(name, table, use_rowid)
|
||||
|
||||
sortable_columns = self.sortable_columns_for_table(name, table, use_rowid)
|
||||
|
||||
# Allow for custom sort order
|
||||
sort = special_args.get("_sort")
|
||||
|
|
@ -713,7 +731,7 @@ class TableView(RowTableShared):
|
|||
pass
|
||||
|
||||
# human_description_en combines filters AND search, if provided
|
||||
human_description_en = filters.human_description_en(extra=search_descriptions)
|
||||
human_description_en = filters.human_description_en(extra=human_description_extras)
|
||||
|
||||
if sort or sort_desc:
|
||||
sorted_by = "sorted by {}{}".format(
|
||||
|
|
|
|||
|
|
@ -69,6 +69,30 @@ And then populate it like this:
|
|||
|
||||
You can use this technique to populate the full-text search index from any combination of tables and joins that makes sense for your project.
|
||||
|
||||
Configuring full-text search for a table or view
|
||||
------------------------------------------------
|
||||
|
||||
If a table has a corresponding FTS table set up using the ``content=`` argument to ``CREATE VIRTUAL TABLE`` shown above, Datasette will detect it automatically and add a search interface to the table page for that table.
|
||||
|
||||
You can also manually configure which table should be used for full-text search using :ref:`metadata`. You can set the associated FTS table for a specific table and you can also set one for a view - if you do that, the page for that SQL view will offer a search option.
|
||||
|
||||
The ``fts_table`` property can be used to specify an associated FTS table. If the primary key column in your table which was used to populate the FTS table is something other than ``rowid``, you can specify the column to use with the ``fts_pk`` property.
|
||||
|
||||
Here is an example which enables full-text search for a ``display_ads`` view which is defined against the ``ads`` table and hence needs to run FTS against the ``ads_fts`` table, using the ``id`` as the primary key::
|
||||
|
||||
{
|
||||
"databases": {
|
||||
"russian-ads": {
|
||||
"tables": {
|
||||
"display_ads": {
|
||||
"fts_table": "ads_fts",
|
||||
"fts_pk": "id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Setting up full-text search using csvs-to-sqlite
|
||||
------------------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -121,6 +121,23 @@ This will restrict sorting of ``example_table`` to just the ``height`` and
|
|||
|
||||
You can also disable sorting entirely by setting ``"sortable_columns": []``
|
||||
|
||||
By default, database views in Datasette do not support sorting. You can use ``sortable_columns`` to enable specific sort orders for a view called ``name_of_view`` in the database ``my_database`` like so::
|
||||
|
||||
{
|
||||
"databases": {
|
||||
"my_database": {
|
||||
"tables": {
|
||||
"name_of_view": {
|
||||
"sortable_columns": [
|
||||
"clicks",
|
||||
"impressions"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. _label_columns:
|
||||
|
||||
Specifying the label column for a table
|
||||
|
|
|
|||
|
|
@ -320,3 +320,28 @@ If the value matches that pattern, the plugin returns an HTML link element:
|
|||
href=jinja2.escape(data["href"]),
|
||||
label=jinja2.escape(data["label"] or "") or " "
|
||||
))
|
||||
|
||||
table_filter
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Apply additional SQL filters to the current table based on the request.
|
||||
|
||||
This should return an awaitable function with the following signature:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from datasette.utils import TableFilter
|
||||
|
||||
@hookimpl
|
||||
def table_filter():
|
||||
async def inner(view, name, table, request):
|
||||
extra_human_descriptions = []
|
||||
where_clauses = []
|
||||
params = {}
|
||||
# ... build those things here
|
||||
return TableFilter(
|
||||
human_description_extras=extra_human_descriptions,
|
||||
where_clauses=where_clauses,
|
||||
params=params,
|
||||
)
|
||||
return inner
|
||||
|
|
|
|||
|
|
@ -158,6 +158,9 @@ METADATA = {
|
|||
'primary_key_multiple_columns_explicit_label': {
|
||||
'label_column': 'content2',
|
||||
},
|
||||
'simple_view': {
|
||||
'sortable_columns': ['content'],
|
||||
}
|
||||
},
|
||||
'queries': {
|
||||
'pragma_cache_size': 'PRAGMA cache_size;',
|
||||
|
|
|
|||
|
|
@ -40,11 +40,6 @@ def test_sql_time_limit(app_client_shorter_time_limit):
|
|||
assert expected_html_fragment in response.text
|
||||
|
||||
|
||||
def test_view(app_client):
|
||||
response = app_client.get('/fixtures/simple_view')
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_row(app_client):
|
||||
response = app_client.get(
|
||||
'/fixtures/simple_primary_key/1',
|
||||
|
|
@ -618,25 +613,33 @@ def test_compound_primary_key_with_foreign_key_references(app_client):
|
|||
|
||||
|
||||
def test_view_html(app_client):
|
||||
response = app_client.get('/fixtures/simple_view')
|
||||
response = app_client.get("/fixtures/simple_view")
|
||||
assert response.status == 200
|
||||
table = Soup(response.body, 'html.parser').find('table')
|
||||
assert [
|
||||
'content', 'upper_content'
|
||||
] == [th.string.strip() for th in table.select('thead th')]
|
||||
table = Soup(response.body, "html.parser").find("table")
|
||||
ths = table.select("thead th")
|
||||
assert 2 == len(ths)
|
||||
assert ths[0].find("a") is not None
|
||||
assert ths[0].find("a")["href"].endswith("/simple_view?_sort=content")
|
||||
assert ths[0].find("a").string.strip() == "content"
|
||||
assert ths[1].find("a") is None
|
||||
assert ths[1].string.strip() == "upper_content"
|
||||
expected = [
|
||||
[
|
||||
'<td class="col-content">hello</td>',
|
||||
'<td class="col-upper_content">HELLO</td>'
|
||||
], [
|
||||
'<td class="col-upper_content">HELLO</td>',
|
||||
],
|
||||
[
|
||||
'<td class="col-content">world</td>',
|
||||
'<td class="col-upper_content">WORLD</td>'
|
||||
], [
|
||||
'<td class="col-upper_content">WORLD</td>',
|
||||
],
|
||||
[
|
||||
'<td class="col-content">\xa0</td>',
|
||||
'<td class="col-upper_content">\xa0</td>'
|
||||
]
|
||||
'<td class="col-upper_content">\xa0</td>',
|
||||
],
|
||||
]
|
||||
assert expected == [
|
||||
[str(td) for td in tr.select("td")] for tr in table.select("tbody tr")
|
||||
]
|
||||
assert expected == [[str(td) for td in tr.select('td')] for tr in table.select('tbody tr')]
|
||||
|
||||
|
||||
def test_index_metadata(app_client):
|
||||
|
|
@ -710,6 +713,10 @@ def test_allow_sql_on(app_client):
|
|||
)
|
||||
soup = Soup(response.body, 'html.parser')
|
||||
assert len(soup.findAll('textarea', {'name': 'sql'}))
|
||||
response = app_client.get(
|
||||
"/fixtures/sortable"
|
||||
)
|
||||
assert b"View and edit SQL" in response.body
|
||||
|
||||
|
||||
def test_allow_sql_off():
|
||||
|
|
@ -721,6 +728,11 @@ def test_allow_sql_off():
|
|||
)
|
||||
soup = Soup(response.body, 'html.parser')
|
||||
assert not len(soup.findAll('textarea', {'name': 'sql'}))
|
||||
# The table page should no longer show "View and edit SQL"
|
||||
response = client.get(
|
||||
"/fixtures/sortable"
|
||||
)
|
||||
assert b"View and edit SQL" not in response.body
|
||||
|
||||
|
||||
def assert_querystring_equal(expected, actual):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue