mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
parent
e11cb4c664
commit
bc6a9b4564
6 changed files with 92 additions and 2 deletions
|
|
@ -105,6 +105,11 @@ h2 em {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
}
|
}
|
||||||
|
.extra-wheres ul, .extra-wheres li {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
form.sql textarea {
|
form.sql textarea {
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
width: 70%;
|
width: 70%;
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,17 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% if extra_wheres_for_ui %}
|
||||||
|
<div class="extra-wheres">
|
||||||
|
<h3>{{ extra_wheres_for_ui|length }} extra where clause{% if extra_wheres_for_ui|length != 1 %}s{% endif %}</h3>
|
||||||
|
<ul>
|
||||||
|
{% for extra_where in extra_wheres_for_ui %}
|
||||||
|
<li><code>{{ extra_where.text }}</code> [<a href="{{ extra_where.remove_url }}">remove</a>]</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if query.sql and config.allow_sql %}
|
{% if query.sql and config.allow_sql %}
|
||||||
<p><a class="not-underlined" title="{{ query.sql }}" href="{{ database_url(database) }}?{{ {'sql': query.sql}|urlencode|safe }}{% if query.params %}&{{ query.params|urlencode|safe }}{% endif %}">✎ <span class="underlined">View and edit SQL</span></a></p>
|
<p><a class="not-underlined" title="{{ query.sql }}" href="{{ database_url(database) }}?{{ {'sql': query.sql}|urlencode|safe }}{% if query.params %}&{{ query.params|urlencode|safe }}{% endif %}">✎ <span class="underlined">View and edit SQL</span></a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,20 @@ class TableView(RowTableShared):
|
||||||
filters = Filters(sorted(other_args.items()), units, ureg)
|
filters = Filters(sorted(other_args.items()), units, ureg)
|
||||||
where_clauses, params = filters.build_where_clauses(table)
|
where_clauses, params = filters.build_where_clauses(table)
|
||||||
|
|
||||||
|
extra_wheres_for_ui = []
|
||||||
|
# Add _where= from querystring
|
||||||
|
if "_where" in request.args:
|
||||||
|
if not self.ds.config("allow_sql"):
|
||||||
|
raise DatasetteError("_where= is not allowed", status=400)
|
||||||
|
else:
|
||||||
|
where_clauses.extend(request.args["_where"])
|
||||||
|
extra_wheres_for_ui = [{
|
||||||
|
"text": text,
|
||||||
|
"remove_url": path_with_removed_args(
|
||||||
|
request, {"_where": text}
|
||||||
|
)
|
||||||
|
} for text in request.args["_where"]]
|
||||||
|
|
||||||
# _search support:
|
# _search support:
|
||||||
fts_table = special_args.get("_fts_table")
|
fts_table = special_args.get("_fts_table")
|
||||||
fts_table = fts_table or table_metadata.get("fts_table")
|
fts_table = fts_table or table_metadata.get("fts_table")
|
||||||
|
|
@ -751,6 +765,7 @@ class TableView(RowTableShared):
|
||||||
key=lambda f: (len(f["results"]), f["name"]),
|
key=lambda f: (len(f["results"]), f["name"]),
|
||||||
reverse=True
|
reverse=True
|
||||||
),
|
),
|
||||||
|
"extra_wheres_for_ui": extra_wheres_for_ui,
|
||||||
"form_hidden_args": form_hidden_args,
|
"form_hidden_args": form_hidden_args,
|
||||||
"facet_hideable": lambda facet: facet not in metadata_facets,
|
"facet_hideable": lambda facet: facet not in metadata_facets,
|
||||||
"is_sortable": any(c["sortable"] for c in display_columns),
|
"is_sortable": any(c["sortable"] for c in display_columns),
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,21 @@ The Datasette table view takes a number of special querystring arguments:
|
||||||
Like ``_search=`` but allows you to specify the column to be searched, as
|
Like ``_search=`` but allows you to specify the column to be searched, as
|
||||||
opposed to searching all columns that have been indexed by FTS.
|
opposed to searching all columns that have been indexed by FTS.
|
||||||
|
|
||||||
|
``?_where=SQL-fragment``
|
||||||
|
If the :ref:`config_allow_sql` config option is enabled, this parameter
|
||||||
|
can be used to pass one or more additional SQL fragments to be used in the
|
||||||
|
`WHERE` clause of the SQL used to query the table.
|
||||||
|
|
||||||
|
This is particularly useful if you are building a JavaScript application
|
||||||
|
that needs to do something creative but still wants the other conveniences
|
||||||
|
provided by the table view (such as faceting) and hence would like not to
|
||||||
|
have to construct a completely custom SQL query.
|
||||||
|
|
||||||
|
Some examples:
|
||||||
|
|
||||||
|
* `facetable?_where=state="MI"&_where=city_id=3 <https://latest.datasette.io/facetable?_where=state=%22MI%22&_where=city_id=3>`__
|
||||||
|
* `facetable?_where=city_id in (select id from facet_cities where name != "Detroit") <https://latest.datasette.io/fixtures/facetable?_where=city_id%20in%20(select%20id%20from%20facet_cities%20where%20name%20!=%20%22Detroit%22)>`__
|
||||||
|
|
||||||
``?_group_count=COLUMN``
|
``?_group_count=COLUMN``
|
||||||
Executes a SQL query that returns a count of the number of rows matching
|
Executes a SQL query that returns a count of the number of rows matching
|
||||||
each unique value in that column, with the most common ordered first.
|
each unique value in that column, with the most common ordered first.
|
||||||
|
|
|
||||||
|
|
@ -443,9 +443,11 @@ def test_allow_sql_off():
|
||||||
for client in make_app_client(config={
|
for client in make_app_client(config={
|
||||||
'allow_sql': False,
|
'allow_sql': False,
|
||||||
}):
|
}):
|
||||||
assert 400 == client.get(
|
response = client.get(
|
||||||
"/fixtures.json?sql=select+sleep(0.01)"
|
"/fixtures.json?sql=select+sleep(0.01)"
|
||||||
).status
|
)
|
||||||
|
assert 400 == response.status
|
||||||
|
assert 'sql= is not allowed' == response.json['error']
|
||||||
|
|
||||||
|
|
||||||
def test_table_json(app_client):
|
def test_table_json(app_client):
|
||||||
|
|
@ -913,6 +915,34 @@ def test_table_filter_json_arraycontains(app_client):
|
||||||
] == response.json['rows']
|
] == response.json['rows']
|
||||||
|
|
||||||
|
|
||||||
|
def test_table_filter_extra_where(app_client):
|
||||||
|
response = app_client.get(
|
||||||
|
"/fixtures/facetable.json?_where=neighborhood='Dogpatch'"
|
||||||
|
)
|
||||||
|
assert [
|
||||||
|
[2, 1, 1, 'CA', 1, 'Dogpatch', '["tag1", "tag3"]']
|
||||||
|
] == response.json['rows']
|
||||||
|
|
||||||
|
|
||||||
|
def test_table_filter_extra_where_invalid(app_client):
|
||||||
|
response = app_client.get(
|
||||||
|
"/fixtures/facetable.json?_where=neighborhood=Dogpatch'"
|
||||||
|
)
|
||||||
|
assert 400 == response.status
|
||||||
|
assert 'Invalid SQL' == response.json['title']
|
||||||
|
|
||||||
|
|
||||||
|
def test_table_filter_extra_where_disabled_if_no_sql_allowed():
|
||||||
|
for client in make_app_client(config={
|
||||||
|
'allow_sql': False,
|
||||||
|
}):
|
||||||
|
response = client.get(
|
||||||
|
"/fixtures/facetable.json?_where=neighborhood='Dogpatch'"
|
||||||
|
)
|
||||||
|
assert 400 == response.status
|
||||||
|
assert '_where= is not allowed' == response.json['error']
|
||||||
|
|
||||||
|
|
||||||
def test_max_returned_rows(app_client):
|
def test_max_returned_rows(app_client):
|
||||||
response = app_client.get(
|
response = app_client.get(
|
||||||
'/fixtures.json?sql=select+content+from+no_primary_key'
|
'/fixtures.json?sql=select+content+from+no_primary_key'
|
||||||
|
|
|
||||||
|
|
@ -869,3 +869,17 @@ def test_show_hide_sql_query(app_client):
|
||||||
] == [
|
] == [
|
||||||
(hidden['name'], hidden['value']) for hidden in hiddens
|
(hidden['name'], hidden['value']) for hidden in hiddens
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_extra_where_clauses(app_client):
|
||||||
|
response = app_client.get(
|
||||||
|
"/fixtures/facetable?_where=neighborhood='Dogpatch'&_where=city_id=1"
|
||||||
|
)
|
||||||
|
soup = Soup(response.body, "html.parser")
|
||||||
|
div = soup.select(".extra-wheres")[0]
|
||||||
|
assert "2 extra where clauses" == div.find("h3").text
|
||||||
|
hrefs = [a["href"] for a in div.findAll("a")]
|
||||||
|
assert [
|
||||||
|
"/fixtures/facetable?_where=city_id%3D1",
|
||||||
|
"/fixtures/facetable?_where=neighborhood%3D%27Dogpatch%27"
|
||||||
|
] == hrefs
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue