Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions datasette/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1820,6 +1820,7 @@ async def menu_links():
"base_url": self.setting("base_url"),
"csrftoken": request.scope["csrftoken"] if request else lambda: "",
"datasette_version": __version__,
"default_deny": self.default_deny,
},
**extra_template_vars,
}
Expand Down
2 changes: 1 addition & 1 deletion datasette/templates/api_explorer.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

{% block content %}

<h1>API Explorer{% if private %} 🔒{% endif %}</h1>
<h1>API Explorer{% if default_deny and not private %} 🌐{% elif not default_deny and private %} 🔒{% endif %}</h1>

<p>Use this tool to try out the
{% if datasette_version %}
Expand Down
8 changes: 4 additions & 4 deletions datasette/templates/database.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

{% block content %}
<div class="page-header" style="border-color: #{{ database_color }}">
<h1>{{ metadata.title or database }}{% if private %} 🔒{% endif %}</h1>
<h1>{{ metadata.title or database }}{% if default_deny and not private %} 🌐{% elif not default_deny and private %} 🔒{% endif %}</h1>
</div>
{% set action_links, action_title = database_actions(), "Database actions" %}
{% include "_action_menu.html" %}
Expand Down Expand Up @@ -50,7 +50,7 @@ <h3>Custom SQL query</h3>
<h2 id="queries">Queries</h2>
<ul class="bullets">
{% for query in queries %}
<li><a href="{{ urls.query(database, query.name) }}{% if query.fragment %}#{{ query.fragment }}{% endif %}" title="{{ query.description or query.sql }}">{{ query.title or query.name }}</a>{% if query.private %} 🔒{% endif %}</li>
<li><a href="{{ urls.query(database, query.name) }}{% if query.fragment %}#{{ query.fragment }}{% endif %}" title="{{ query.description or query.sql }}">{{ query.title or query.name }}</a>{% if default_deny and not query.private %} 🌐{% elif not default_deny and query.private %} 🔒{% endif %}</li>
{% endfor %}
</ul>
{% endif %}
Expand All @@ -62,7 +62,7 @@ <h2 id="tables">Tables <a style="font-weight: normal; font-size: 0.75em; padding
{% for table in tables %}
{% if show_hidden or not table.hidden %}
<div class="db-table">
<h3><a href="{{ urls.table(database, table.name) }}">{{ table.name }}</a>{% if table.private %} 🔒{% endif %}{% if table.hidden %}<em> (hidden)</em>{% endif %}</h3>
<h3><a href="{{ urls.table(database, table.name) }}">{{ table.name }}</a>{% if default_deny and not table.private %} 🌐{% elif not default_deny and table.private %} 🔒{% endif %}{% if table.hidden %}<em> (hidden)</em>{% endif %}</h3>
<p><em>{% for column in table.columns %}{{ column }}{% if not loop.last %}, {% endif %}{% endfor %}</em></p>
<p>{% if table.count is none %}Many rows{% elif table.count == count_limit + 1 %}&gt;{{ "{:,}".format(count_limit) }} rows{% else %}{{ "{:,}".format(table.count) }} row{% if table.count == 1 %}{% else %}s{% endif %}{% endif %}</p>
</div>
Expand All @@ -77,7 +77,7 @@ <h3><a href="{{ urls.table(database, table.name) }}">{{ table.name }}</a>{% if t
<h2 id="views">Views</h2>
<ul class="bullets">
{% for view in views %}
<li><a href="{{ urls.database(database) }}/{{ view.name|urlencode }}">{{ view.name }}</a>{% if view.private %} 🔒{% endif %}</li>
<li><a href="{{ urls.database(database) }}/{{ view.name|urlencode }}">{{ view.name }}</a>{% if default_deny and not view.private %} 🌐{% elif not default_deny and view.private %} 🔒{% endif %}</li>
{% endfor %}
</ul>
{% endif %}
Expand Down
6 changes: 3 additions & 3 deletions datasette/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{% block body_class %}index{% endblock %}

{% block content %}
<h1>{{ metadata.title or "Datasette" }}{% if private %} 🔒{% endif %}</h1>
<h1>{{ metadata.title or "Datasette" }}{% if default_deny and not private %} 🌐{% elif not default_deny and private %} 🔒{% endif %}</h1>

{% set action_links, action_title = homepage_actions, "Homepage actions" %}
{% include "_action_menu.html" %}
Expand All @@ -19,7 +19,7 @@ <h1>{{ metadata.title or "Datasette" }}{% if private %} 🔒{% endif %}</h1>
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}

{% for database in databases %}
<h2 style="padding-left: 10px; border-left: 10px solid #{{ database.color }}"><a href="{{ urls.database(database.name) }}">{{ database.name }}</a>{% if database.private %} 🔒{% endif %}</h2>
<h2 style="padding-left: 10px; border-left: 10px solid #{{ database.color }}"><a href="{{ urls.database(database.name) }}">{{ database.name }}</a>{% if default_deny and not database.private %} 🌐{% elif not default_deny and database.private %} 🔒{% endif %}</h2>
<p>
{% if database.show_table_row_counts %}{{ "{:,}".format(database.table_rows_sum) }} rows in {% endif %}{{ database.tables_count }} table{% if database.tables_count != 1 %}s{% endif %}{% if database.hidden_tables_count %}, {% endif -%}
{% if database.hidden_tables_count -%}
Expand All @@ -30,7 +30,7 @@ <h2 style="padding-left: 10px; border-left: 10px solid #{{ database.color }}"><a
{{ "{:,}".format(database.views_count) }} view{% if database.views_count != 1 %}s{% endif %}
{% endif %}
</p>
<p>{% for table in database.tables_and_views_truncated %}<a href="{{ urls.table(database.name, table.name) }}"{% if table.count %} title="{{ table.count }} rows"{% endif %}>{{ table.name }}</a>{% if table.private %} 🔒{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if database.tables_and_views_more %}, <a href="{{ urls.database(database.name) }}">...</a>{% endif %}</p>
<p>{% for table in database.tables_and_views_truncated %}<a href="{{ urls.table(database.name, table.name) }}"{% if table.count %} title="{{ table.count }} rows"{% endif %}>{{ table.name }}</a>{% if default_deny and not table.private %} 🌐{% elif not default_deny and table.private %} 🔒{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if database.tables_and_views_more %}, <a href="{{ urls.database(database.name) }}">...</a>{% endif %}</p>
{% endfor %}

{% endblock %}
2 changes: 1 addition & 1 deletion datasette/templates/query.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<p class="message-error">This query cannot be executed because the database is immutable.</p>
{% endif %}

<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">{{ metadata.title or database }}{% if canned_query and not metadata.title %}: {{ canned_query }}{% endif %}{% if private %} 🔒{% endif %}</h1>
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">{{ metadata.title or database }}{% if canned_query and not metadata.title %}: {{ canned_query }}{% endif %}{% if default_deny and not private %} 🌐{% elif not default_deny and private %} 🔒{% endif %}</h1>
{% set action_links, action_title = query_actions(), "Query actions" %}
{% include "_action_menu.html" %}

Expand Down
2 changes: 1 addition & 1 deletion datasette/templates/row.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
{% endblock %}

{% block content %}
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">{{ table }}: {{ ', '.join(primary_key_values) }}{% if private %} 🔒{% endif %}</h1>
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">{{ table }}: {{ ', '.join(primary_key_values) }}{% if default_deny and not private %} 🌐{% elif not default_deny and private %} 🔒{% endif %}</h1>

{% set action_links, action_title = row_actions, "Row actions" %}
{% include "_action_menu.html" %}
Expand Down
2 changes: 1 addition & 1 deletion datasette/templates/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

{% block content %}
<div class="page-header" style="border-color: #{{ database_color }}">
<h1>{{ metadata.get("title") or table }}{% if is_view %} (view){% endif %}{% if private %} 🔒{% endif %}</h1>
<h1>{{ metadata.get("title") or table }}{% if is_view %} (view){% endif %}{% if default_deny and not private %} 🌐{% elif not default_deny and private %} 🔒{% endif %}</h1>
</div>
{% set action_links, action_title = actions(), "View actions" if is_view else "Table actions" %}
{% include "_action_menu.html" %}
Expand Down
43 changes: 27 additions & 16 deletions datasette/views/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,23 +248,34 @@ async def display_columns_and_rows(
display_value = plugin_display_value
elif isinstance(value, bytes):
formatted = format_bytes(len(value))
display_value = markupsafe.Markup(
'<a class="blob-download" href="{}"{}>&lt;Binary:&nbsp;{:,}&nbsp;byte{}&gt;</a>'.format(
datasette.urls.row_blob(
database_name,
table_name,
path_from_row_pks(row, pks, not pks),
column,
),
(
' title="{}"'.format(formatted)
if "bytes" not in formatted
else ""
),
len(value),
"" if len(value) == 1 else "s",
)
title = (
' title="{}"'.format(formatted) if "bytes" not in formatted else ""
)
try:
blob_path = path_from_row_pks(row, pks, not pks)
except (IndexError, KeyError):
blob_path = None
if blob_path is not None:
display_value = markupsafe.Markup(
'<a class="blob-download" href="{}"{}>&lt;Binary:&nbsp;{:,}&nbsp;byte{}&gt;</a>'.format(
datasette.urls.row_blob(
database_name,
table_name,
blob_path,
column,
),
title,
len(value),
"" if len(value) == 1 else "s",
)
)
else:
display_value = markupsafe.Markup(
"&lt;Binary:&nbsp;{:,}&nbsp;byte{}&gt;".format(
len(value),
"" if len(value) == 1 else "s",
)
)
elif isinstance(value, dict):
# It's an expanded foreign key - display link to other row
label = value["label"]
Expand Down
33 changes: 33 additions & 0 deletions tests/test_default_deny.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,39 @@ async def test_default_deny_with_config_allow():
assert await ds.allowed(action="view-instance", actor={"id": "user2"}) is False


@pytest.mark.asyncio
async def test_default_deny_shows_public_icons_not_private_icons():
ds = Datasette(
default_deny=True,
config={
"allow": {"id": "user1"},
"databases": {
"test_db": {
"allow": True,
"tables": {
"public_table": {"allow": True},
"private_table": {"allow": {"id": "user1"}},
},
}
},
},
)
await ds.invoke_startup()

db = ds.add_memory_database("test_db")
await db.execute_write("create table public_table (id integer primary key)")
await db.execute_write("create table private_table (id integer primary key)")
await ds._refresh_schemas()

cookie = ds.client.actor_cookie({"id": "user1"})
response = await ds.client.get("/test_db", cookies={"ds_actor": cookie})
assert response.status_code == 200

# In --default-deny mode show public icons, not private padlocks
assert ">public_table</a> 🌐</h3>" in response.text
assert ">private_table</a> 🔒</h3>" not in response.text


@pytest.mark.asyncio
async def test_default_deny_basic_permissions():
"""Test that default_deny=True denies basic permissions"""
Expand Down
44 changes: 44 additions & 0 deletions tests/test_table_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,50 @@ async def test_binary_data_display_in_table(ds_client):
]


@pytest.mark.asyncio
async def test_blob_column_in_join_view_does_not_error(tmp_path):
from datasette.utils.sqlite import sqlite3
from datasette.views.table import display_columns_and_rows

db_path = str(tmp_path / "data.db")
conn = sqlite3.connect(db_path)
conn.executescript("""
create table foo(term, value);
create table bar(term, definition, bytes);

insert into foo values ('text one', 1), ('text two', 2);
insert into bar values
('text one', 'definition one', x'8af8ab88'),
('text two', 'definition two', x'98246547');

create view foo_view as
select foo.*, bar.* from foo join bar on foo.term = bar.term;
""")
conn.close()

ds = Datasette([db_path])
await ds.invoke_startup()
internal_db = ds.get_internal_database()
await internal_db.execute_write(
"create table if not exists metadata_columns (database_name text, resource_name text, column_name text, key text, value text)"
)
db = ds.databases["data"]
results = await db.execute("select * from foo_view")
description = [(column_name,) for column_name in results.columns]

_, cell_rows = await display_columns_and_rows(
ds,
"data",
"foo_view",
description,
results.rows,
link_column=False,
)

binary_cell = next(cell for cell in cell_rows[0] if cell["column"] == "bytes")
assert "&lt;Binary" in str(binary_cell["value"])


def test_custom_table_include():
with make_app_client(
template_dir=str(pathlib.Path(__file__).parent / "test_templates")
Expand Down