Skip to content

Commit

Permalink
Switch URLs to /db/-/write, closes #12
Browse files Browse the repository at this point in the history
Also drop menu_links in favor of database_actions

And class=core, refs simonw/datasette#2417
  • Loading branch information
simonw committed Sep 3, 2024
1 parent f828e15 commit a51ff11
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 64 deletions.
59 changes: 20 additions & 39 deletions datasette_write/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,18 @@ async def write(request, datasette):
request.actor, "datasette-write", default=False
):
raise Forbidden("Permission denied for datasette-write")
databases = [
db
for db in datasette.databases.values()
if db.is_mutable and db.name != "_internal"
]
database_name = request.url_vars["database"]
if request.method == "GET":
selected_database = request.args.get("database") or ""
if not selected_database or selected_database == "_internal":
selected_database = databases[0].name
database = datasette.get_database(selected_database)
database = datasette.get_database(database_name)
tables = await database.table_names()
views = await database.view_names()
sql = request.args.get("sql") or ""
return Response.html(
await datasette.render_template(
"datasette_write.html",
{
"databases": databases,
"sql_from_args": sql,
"selected_database": selected_database,
"database_name": database_name,
"parameters": await derive_parameters(database, sql),
"tables": tables,
"views": views,
Expand All @@ -38,12 +30,8 @@ async def write(request, datasette):
)
elif request.method == "POST":
formdata = await request.post_vars()
database_name = formdata["database"]
sql = formdata["sql"]
try:
database = [db for db in databases if db.name == database_name][0]
except IndexError:
return Response.html("Database not found", status_code=404)
database = datasette.get_database(database_name)

result = None
message = None
Expand Down Expand Up @@ -92,6 +80,19 @@ async def write(request, datasette):
return Response.html("Bad method", status_code=405)


async def write_redirect(request, datasette):
if not await datasette.permission_allowed(
request.actor, "datasette-write", default=False
):
raise Forbidden("Permission denied for datasette-write")

db = request.args.get("database") or ""
if not db:
db = datasette.get_database().name

return Response.redirect(datasette.urls.database(db) + "/-/write")


async def derive_parameters(db, sql):
parameters = await derive_named_parameters(db, sql)
return [
Expand Down Expand Up @@ -124,7 +125,8 @@ async def write_derive_parameters(datasette, request):
@hookimpl
def register_routes():
return [
(r"^/-/write$", write),
(r"^/(?P<database>[^/]+)/-/write$", write),
(r"^/-/write$", write_redirect),
(r"^/-/write/derive-parameters$", write_derive_parameters),
]

Expand All @@ -135,20 +137,6 @@ def permission_allowed(actor, action):
return True


@hookimpl
def menu_links(datasette, actor):
async def inner():
if await datasette.permission_allowed(actor, "datasette-write", default=False):
return [
{
"href": datasette.urls.path("/-/write"),
"label": "Execute SQL write",
},
]

return inner


@hookimpl
def database_actions(datasette, actor, database):
async def inner():
Expand All @@ -157,14 +145,7 @@ async def inner():
):
return [
{
"href": datasette.urls.path(
"/-/write?"
+ urlencode(
{
"database": database,
}
)
),
"href": datasette.urls.database(database) + "/-/write",
"label": "Execute SQL write",
"description": "Run queries like insert/update/delete against this database",
},
Expand Down
15 changes: 8 additions & 7 deletions datasette_write/templates/datasette_write.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@
</style>
{% endblock %}

{% block crumbs %}
{{ crumbs.nav(request=request, database=database_name) }}
{% endblock %}

{% block content %}
<h1>Write to the database with SQL</h1>
<h1>Write to {{ database_name }} with SQL</h1>

<form class="write-form" action="{{ base_url }}-/write" method="post" style="margin-bottom: 1em">
<form class="write-form core" action="{{ request.path }}" method="post" style="margin-bottom: 1em">
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
<p class="database-select"><label>Database: <select name="database">{% for database in databases %}
<option{% if database.name == selected_database %} selected="selected"{% endif %}>{{ database.name }}</option>
{% endfor %}</select></label></p>
<p><textarea name="sql" style="box-sizing: border-box; width: 100%; padding-right: 10px; max-width: 600px; height: 10em; padding: 6px;">{{ sql_from_args }}</textarea></p>
<div class="query-parameters">
<div id="query-parameters-area">
Expand All @@ -65,15 +66,15 @@ <h1>Write to the database with SQL</h1>
{% if tables %}
<p><strong>Tables</strong>:
{% for table in tables %}
<a href="{{ urls.table(selected_database, table) }}">{{ table }}</a>{% if not loop.last %}, {% endif %}
<a href="{{ urls.table(database_name, table) }}">{{ table }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}

{% if views %}
<p><strong>Views</strong>:
{% for view in views %}
<a href="{{ urls.table(selected_database, view) }}">{{ view }}</a>{% if not loop.last %}, {% endif %}
<a href="{{ urls.table(database_name, view) }}">{{ view }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}
Expand Down
24 changes: 6 additions & 18 deletions tests/test_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ def ds(tmp_path_factory):

@pytest.mark.asyncio
async def test_permission_denied(ds):
response = await ds.client.get("/-/write")
response = await ds.client.get("/test/-/write")
assert 403 == response.status_code


@pytest.mark.asyncio
async def test_permission_granted_to_root(ds):
response = await ds.client.get(
"/-/write",
"/test/-/write",
cookies={"ds_actor": ds.sign({"a": {"id": "root"}}, "actor")},
)
assert response.status_code == 200
Expand All @@ -40,7 +40,7 @@ async def test_permission_granted_to_root(ds):

# Should have database action menu option too:
anon_response = (await ds.client.get("/test")).text
fragment = ">Execute SQL write<"
fragment = '<a href="/test/-/write">Execute SQL write'
assert fragment not in anon_response
root_response = (
await ds.client.get(
Expand All @@ -50,21 +50,10 @@ async def test_permission_granted_to_root(ds):
assert fragment in root_response


@pytest.mark.asyncio
@pytest.mark.parametrize("database", ["test", "test2"])
async def test_select_database(ds, database):
response = await ds.client.get(
"/-/write?database={}".format(database),
cookies={"ds_actor": ds.sign({"a": {"id": "root"}}, "actor")},
)
assert response.status_code == 200
assert '<option selected="selected">{}</option>'.format(database) in response.text


@pytest.mark.asyncio
async def test_populate_sql_from_query_string(ds):
response = await ds.client.get(
"/-/write?sql=select+1",
"/test/-/write?sql=select+1",
cookies={"ds_actor": ds.sign({"a": {"id": "root"}}, "actor")},
)
assert response.status_code == 200
Expand Down Expand Up @@ -121,19 +110,18 @@ async def test_populate_sql_from_query_string(ds):
async def test_execute_write(ds, database, sql, params, expected_message):
# Get csrftoken
cookies = {"ds_actor": ds.sign({"a": {"id": "root"}}, "actor")}
response = await ds.client.get("/-/write", cookies=cookies)
response = await ds.client.get("/{}/-/write".format(database), cookies=cookies)
assert 200 == response.status_code
csrftoken = response.cookies["ds_csrftoken"]
cookies["ds_csrftoken"] = csrftoken
data = {
"sql": sql,
"csrftoken": csrftoken,
"database": database,
}
data.update(params)
# write to database
response2 = await ds.client.post(
"/-/write",
"/{}/-/write".format(database),
data=data,
cookies=cookies,
)
Expand Down

0 comments on commit a51ff11

Please sign in to comment.