Compare commits

...

12 commits

Author SHA1 Message Date
Simon Willison
b76a90e871 Added messages to pattern portfolio, refs #790 2020-06-02 16:56:46 -07:00
Simon Willison
69aa0277f5 /-/plugins now shows details of hooks, closes #794
Also added /-/plugins?all=1 parameter to see default plugins.
2020-06-02 16:56:39 -07:00
Simon Willison
9dd6d1ae6d More consistent use of response.text/response.json in tests, closes #792 2020-06-02 16:56:26 -07:00
Simon Willison
61e40e917e Flash messages mechanism, closes #790 2020-06-02 14:08:12 -07:00
Simon Willison
29b8e80f3c New request.cookies property 2020-06-02 14:06:53 -07:00
Simon Willison
2c3f086e98 permission_checks is now _permission_checks 2020-06-02 10:43:50 -07:00
Simon Willison
6690d6bf7f Internal scope correlation ID, refs #703 2020-06-01 08:47:16 -07:00
Simon Willison
61131f1fd7 Merge branch 'writable-canned' of github.com:simonw/datasette into writable-canned 2020-06-01 07:54:35 -07:00
Simon Willison
ee51e0b82c Applied black 2020-06-01 07:54:07 -07:00
Simon Willison
5b252a2348 First working version of writable canned queries, refs #698 2020-06-01 07:54:05 -07:00
Simon Willison
95bb5181b4 Applied black 2020-04-29 00:50:55 -07:00
Simon Willison
18c5bd1805 First working version of writable canned queries, refs #698 2020-04-29 00:47:12 -07:00
24 changed files with 393 additions and 62 deletions

View file

@ -5,14 +5,9 @@ from http.cookies import SimpleCookie
@hookimpl
def actor_from_request(datasette, request):
cookies = SimpleCookie()
cookies.load(
dict(request.scope.get("headers") or []).get(b"cookie", b"").decode("utf-8")
)
if "ds_actor" not in cookies:
if "ds_actor" not in request.cookies:
return None
ds_actor = cookies["ds_actor"].value
try:
return datasette.unsign(ds_actor, "actor")
return datasette.unsign(request.cookies["ds_actor"], "actor")
except BadSignature:
return None

View file

@ -2,6 +2,7 @@ import asyncio
import collections
import datetime
import hashlib
from http.cookies import SimpleCookie
import itertools
import json
import os
@ -10,6 +11,7 @@ import sys
import threading
import traceback
import urllib.parse
import uuid
from concurrent import futures
from pathlib import Path
@ -30,6 +32,7 @@ from .views.special import (
PatternPortfolioView,
AuthTokenView,
PermissionsDebugView,
MessagesDebugView,
)
from .views.table import RowView, TableView
from .renderer import json_renderer
@ -156,6 +159,11 @@ async def favicon(scope, receive, send):
class Datasette:
# Message constants:
INFO = 1
WARNING = 2
ERROR = 3
def __init__(
self,
files,
@ -289,7 +297,7 @@ class Datasette:
pm.hook.prepare_jinja2_environment(env=self.jinja_env)
self._register_renderers()
self.permission_checks = collections.deque(maxlen=30)
self._permission_checks = collections.deque(maxlen=30)
self._root_token = os.urandom(32).hex()
def sign(self, value, namespace="default"):
@ -423,6 +431,38 @@ class Datasette:
# pylint: disable=no-member
pm.hook.prepare_connection(conn=conn, database=database, datasette=self)
def add_message(self, request, message, type=INFO):
if not hasattr(request, "_messages"):
request._messages = []
request._messages_should_clear = False
request._messages.append((message, type))
def _write_messages_to_response(self, request, response):
if getattr(request, "_messages", None):
# Set those messages
cookie = SimpleCookie()
cookie["ds_messages"] = self.sign(request._messages, "messages")
cookie["ds_messages"]["path"] = "/"
# TODO: Co-exist with existing set-cookie headers
assert "set-cookie" not in response.headers
response.headers["set-cookie"] = cookie.output(header="").lstrip()
elif getattr(request, "_messages_should_clear", False):
cookie = SimpleCookie()
cookie["ds_messages"] = ""
cookie["ds_messages"]["path"] = "/"
# TODO: Co-exist with existing set-cookie headers
assert "set-cookie" not in response.headers
response.headers["set-cookie"] = cookie.output(header="").lstrip()
def _show_messages(self, request):
if getattr(request, "_messages", None):
request._messages_should_clear = True
messages = request._messages
request._messages = []
return messages
else:
return []
async def permission_allowed(
self, actor, action, resource_type=None, resource_identifier=None, default=False
):
@ -445,7 +485,7 @@ class Datasette:
if result is None:
result = default
used_default = True
self.permission_checks.append(
self._permission_checks.append(
{
"when": datetime.datetime.utcnow().isoformat(),
"actor": actor,
@ -586,9 +626,9 @@ class Datasette:
},
}
def _plugins(self, show_all=False):
def _plugins(self, request):
ps = list(get_plugins())
if not show_all:
if not request.args.get("all"):
ps = [p for p in ps if p["name"] not in DEFAULT_PLUGINS]
return [
{
@ -596,6 +636,7 @@ class Datasette:
"static": p["static_path"] is not None,
"templates": p["templates_path"] is not None,
"version": p.get("version"),
"hooks": p["hooks"],
}
for p in ps
]
@ -783,7 +824,9 @@ class Datasette:
r"/-/versions(?P<as_format>(\.json)?)$",
)
add_route(
JsonDataView.as_asgi(self, "plugins.json", self._plugins),
JsonDataView.as_asgi(
self, "plugins.json", self._plugins, needs_request=True
),
r"/-/plugins(?P<as_format>(\.json)?)$",
)
add_route(
@ -808,6 +851,9 @@ class Datasette:
add_route(
PermissionsDebugView.as_asgi(self), r"/-/permissions$",
)
add_route(
MessagesDebugView.as_asgi(self), r"/-/messages$",
)
add_route(
PatternPortfolioView.as_asgi(self), r"/-/patterns$",
)
@ -856,13 +902,15 @@ class DatasetteRouter(AsgiRouter):
base_url = self.ds.config("base_url")
if base_url != "/" and path.startswith(base_url):
path = "/" + path[len(base_url) :]
# Add a correlation ID
scope_modifications = {"correlation_id": str(uuid.uuid4())}
# Apply force_https_urls, if set
if (
self.ds.config("force_https_urls")
and scope["type"] == "http"
and scope.get("scheme") != "https"
):
scope = dict(scope, scheme="https")
scope_modifications["scheme"] = "https"
# Handle authentication
actor = None
for actor in pm.hook.actor_from_request(
@ -874,7 +922,10 @@ class DatasetteRouter(AsgiRouter):
actor = await actor
if actor:
break
return await super().route_path(dict(scope, actor=actor), receive, send, path)
scope_modifications["actor"] = actor
return await super().route_path(
dict(scope, **scope_modifications), receive, send, path
)
async def handle_404(self, scope, receive, send, exception=None):
# If URL has a trailing slash, redirect to URL without it

View file

@ -49,6 +49,7 @@ def get_plugins():
"name": plugin.__name__,
"static_path": static_path,
"templates_path": templates_path,
"hooks": [h.name for h in pm.get_hookcallers(plugin)],
}
distinfo = plugin_to_distinfo.get(plugin)
if distinfo:

View file

@ -351,3 +351,19 @@ p.zero-results {
.type-float, .type-int {
color: #666;
}
.message-info {
padding: 1em;
border: 1px solid green;
background-color: #c7fbc7;
}
.message-warning {
padding: 1em;
border: 1px solid #ae7100;
background-color: #fbdda5;
}
.message-error {
padding: 1em;
border: 1px solid red;
background-color: pink;
}

View file

@ -17,6 +17,14 @@
<nav class="hd">{% block nav %}{% endblock %}</nav>
<div class="bd">
{% block messages %}
{% if show_messages %}
{% for message, message_type in show_messages() %}
<p class="message-{% if message_type == 1 %}info{% elif message_type == 2 %}warning{% elif message_type == 3 %}error{% endif %}">{{ message }}</p>
{% endfor %}
{% endif %}
{% endblock %}
{% block content %}
{% endblock %}
</div>

View file

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block title %}Debug messages{% endblock %}
{% block content %}
<h1>Debug messages</h1>
<p>Set a message:</p>
<form action="/-/messages" method="POST">
<div>
<input type="text" name="message" style="width: 40%">
<div class="select-wrapper">
<select name="message_type">
<option>INFO</option>
<option>WARNING</option>
<option>ERROR</option>
<option>all</option>
</select>
</div>
<input type="submit" value="Add message">
</div>
</form>
{% endblock %}

View file

@ -20,6 +20,12 @@
<a href="/fixtures/attraction_characteristic">attraction_characteristic</a>
</p>
</nav>
<h2 class="pattern-heading">Messages</h2>
<div class="bd">
<p class="message-info">Example message</p>
<p class="message-warning">Example message</p>
<p class="message-error">Example message</p>
</div>
<h2 class="pattern-heading">.bd for /</h2>
<div class="bd">
<h1>Datasette Fixtures</h1>

View file

@ -27,11 +27,12 @@
{% endblock %}
{% block content %}
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color(database) }}">{{ metadata.title or database }}</h1>
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
<form class="sql" action="{{ database_url(database) }}{% if canned_query %}/{{ canned_query }}{% endif %}" method="get">
<form class="sql" action="{{ database_url(database) }}{% if canned_query %}/{{ canned_query }}{% endif %}" method="{% if canned_write %}post{% else %}get{% endif %}">
<h3>Custom SQL query{% if display_rows %} returning {% if truncated %}more than {% endif %}{{ "{:,}".format(display_rows|length) }} row{% if display_rows|length == 1 %}{% else %}s{% endif %}{% endif %} <span class="show-hide-sql">{% if hide_sql %}(<a href="{{ path_with_removed_args(request, {'_hide_sql': '1'}) }}">show</a>){% else %}(<a href="{{ path_with_added_args(request, {'_hide_sql': '1'}) }}">hide</a>){% endif %}</span></h3>
{% if not hide_sql %}
{% if editable and config.allow_sql %}
@ -74,7 +75,12 @@
</tbody>
</table>
{% else %}
<p class="zero-results">0 results</p>
{% if success_message %}
<p class="success">{{ success_message }}</p>
{% endif %}
{% if not canned_write %}
<p class="zero-results">0 results</p>
{% endif %}
{% endif %}
{% include "_codemirror_foot.html" %}

View file

@ -4,6 +4,7 @@ from mimetypes import guess_type
from urllib.parse import parse_qs, urlunparse, parse_qsl
from pathlib import Path
from html import escape
from http.cookies import SimpleCookie
import re
import aiofiles
@ -44,6 +45,12 @@ class Request:
def host(self):
return self.headers.get("host") or "localhost"
@property
def cookies(self):
cookies = SimpleCookie()
cookies.load(self.headers.get("cookie", ""))
return {key: value.value for key, value in cookies.items()}
@property
def path(self):
if self.scope.get("raw_path") is not None:
@ -173,9 +180,9 @@ class AsgiLifespan:
class AsgiView:
def dispatch_request(self, request, *args, **kwargs):
async def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None)
return handler(request, *args, **kwargs)
return await handler(request, *args, **kwargs)
@classmethod
def as_asgi(cls, *class_args, **class_kwargs):

View file

@ -1,6 +1,7 @@
import asyncio
import csv
import itertools
from itsdangerous import BadSignature
import json
import re
import time
@ -73,6 +74,20 @@ class BaseView(AsgiView):
def database_color(self, database):
return "ff0000"
async def dispatch_request(self, request, *args, **kwargs):
# Populate request_messages if ds_messages cookie is present
if self.ds:
try:
request._messages = self.ds.unsign(
request.cookies.get("ds_messages", ""), "messages"
)
except BadSignature:
pass
response = await super().dispatch_request(request, *args, **kwargs)
if self.ds:
self.ds._write_messages_to_response(request, response)
return response
async def render(self, templates, request, context=None):
context = context or {}
template = self.ds.jinja_env.select_template(templates)
@ -81,6 +96,7 @@ class BaseView(AsgiView):
**{
"database_url": self.database_url,
"database_color": self.database_color,
"show_messages": lambda: self.ds._show_messages(request),
"select_templates": [
"{}{}".format(
"*" if template_name == template.name else "", template_name

View file

@ -106,6 +106,8 @@ class QueryView(DataView):
canned_query=None,
metadata=None,
_size=None,
named_parameters=None,
write=False,
):
params = {key: request.args.get(key) for key in request.args}
if "sql" in params:
@ -113,7 +115,7 @@ class QueryView(DataView):
if "_shape" in params:
params.pop("_shape")
# Extract any :named parameters
named_parameters = self.re_named_parameter.findall(sql)
named_parameters = named_parameters or self.re_named_parameter.findall(sql)
named_parameter_values = {
named_parameter: params.get(named_parameter) or ""
for named_parameter in named_parameters
@ -129,12 +131,49 @@ class QueryView(DataView):
extra_args["custom_time_limit"] = int(params["_timelimit"])
if _size:
extra_args["page_size"] = _size
results = await self.ds.execute(
database, sql, params, truncate=True, **extra_args
)
columns = [r[0] for r in results.description]
templates = ["query-{}.html".format(to_css_class(database)), "query.html"]
# Execute query - as write or as read
if write:
if request.method == "POST":
params = await request.post_vars()
write_ok = await self.ds.databases[database].execute_write(
sql, params, block=True
)
return self.redirect(
request, request.path + "?_success=Query+executed_successfully"
)
else:
async def extra_template():
return {
"request": request,
"path_with_added_args": path_with_added_args,
"path_with_removed_args": path_with_removed_args,
"named_parameter_values": named_parameter_values,
"canned_query": canned_query,
"success_message": request.raw_args.get("_success") or "",
"canned_write": True,
}
return (
{
"database": database,
"rows": [],
"truncated": False,
"columns": [],
"query": {"sql": sql, "params": params},
},
extra_template,
templates,
)
else: # Not a write
results = await self.ds.execute(
database, sql, params, truncate=True, **extra_args
)
columns = [r[0] for r in results.description]
if canned_query:
templates.insert(
0,

View file

@ -92,5 +92,29 @@ class PermissionsDebugView(BaseView):
return await self.render(
["permissions_debug.html"],
request,
{"permission_checks": reversed(self.ds.permission_checks)},
{"permission_checks": reversed(self.ds._permission_checks)},
)
class MessagesDebugView(BaseView):
name = "messages_debug"
def __init__(self, datasette):
self.ds = datasette
async def get(self, request):
return await self.render(["messages_debug.html"], request)
async def post(self, request):
post = await request.post_vars()
message = post.get("message", "")
message_type = post.get("message_type") or "INFO"
assert message_type in ("INFO", "WARNING", "ERROR", "all")
datasette = self.ds
if message_type == "all":
datasette.add_message(request, message, datasette.INFO)
datasette.add_message(request, message, datasette.WARNING)
datasette.add_message(request, message, datasette.ERROR)
else:
datasette.add_message(request, message, getattr(datasette, message_type))
return Response.redirect("/")

View file

@ -221,6 +221,22 @@ class RowTableShared(DataView):
class TableView(RowTableShared):
name = "table"
async def post(self, request, db_name, table_and_format):
# Handle POST to a canned query
canned_query = self.ds.get_canned_query(db_name, table_and_format)
assert canned_query, "You may only POST to a canned query"
return await QueryView(self.ds).data(
request,
db_name,
None,
canned_query["sql"],
metadata=canned_query,
editable=False,
canned_query=table_and_format,
named_parameters=canned_query.get("params"),
write=bool(canned_query.get("write")),
)
async def data(
self,
request,
@ -241,6 +257,8 @@ class TableView(RowTableShared):
metadata=canned_query,
editable=False,
canned_query=table,
named_parameters=canned_query.get("params"),
write=bool(canned_query.get("write")),
)
db = self.ds.databases[database]

View file

@ -27,6 +27,9 @@ The request object is passed to various plugin hooks. It represents an incoming
``.headers`` - dictionary (str -> str)
A dictionary of incoming HTTP request headers.
``.cookies`` - dictionary (str -> str)
A dictionary of incoming cookies
``.host`` - string
The host header from the incoming request, e.g. ``latest.datasette.io`` or ``localhost``.
@ -211,6 +214,24 @@ This method returns a signed string, which can be decoded and verified using :re
Returns the original, decoded object that was passed to :ref:`datasette_sign`. If the signature is not valid this raises a ``itsdangerous.BadSignature`` exception.
.. _datasette_add_message:
.add_message(request, message, message_type=datasette.INFO)
-----------------------------------------------------------
``request`` - Request
The current Request object
``message`` - string
The message string
``message_type`` - constant, optional
The message type - ``datasette.INFO``, ``datasette.WARNING`` or ``datasette.ERROR``
Datasette's flash messaging mechanism allows you to add a message that will be displayed to the user on the next page that they visit. Messages are persisted in a ``ds_messages`` cookie. This method adds a message to that cookie.
You can try out these messages (including the different visual styling of the three message types) using the ``/-/messages`` debugging tool.
.. _internals_database:
Database class

View file

@ -78,10 +78,13 @@ Shows a list of currently installed plugins and their versions. `Plugins example
"name": "datasette_cluster_map",
"static": true,
"templates": false,
"version": "0.4"
"version": "0.10",
"hooks": ["extra_css_urls", "extra_js_urls", "extra_body_script"]
}
]
Add ``?all=1`` to include details of the default plugins baked into Datasette.
.. _JsonDataView_config:
/-/config
@ -166,3 +169,11 @@ Shows the currently authenticated actor. Useful for debugging Datasette authenti
"username": "some-user"
}
}
.. _MessagesDebugView:
/-/messages
-----------
The debug tool at ``/-/messages`` can be used to set flash messages to try out that feature. See :ref:`datasette_add_message` for details of this feature.

View file

@ -29,6 +29,12 @@ class TestResponse:
self.headers = headers
self.body = body
@property
def cookies(self):
cookie = SimpleCookie()
cookie.load(self.headers.get("set-cookie") or "")
return {key: value.value for key, value in cookie.items()}
@property
def json(self):
return json.loads(self.text)

View file

@ -0,0 +1,21 @@
from datasette import hookimpl
def render_message_debug(datasette, request):
if request.args.get("add_msg"):
msg_type = request.args.get("type", "INFO")
datasette.add_message(
request, request.args["add_msg"], getattr(datasette, msg_type)
)
return {"body": "Hello from message debug"}
@hookimpl
def register_output_renderer(datasette):
return [
{
"extension": "message",
"render": render_message_debug,
"can_render": lambda: False,
}
]

View file

@ -1260,13 +1260,59 @@ def test_threads_json(app_client):
def test_plugins_json(app_client):
response = app_client.get("/-/plugins.json")
expected = [
{"name": name, "static": False, "templates": False, "version": None}
for name in (
"my_plugin.py",
"my_plugin_2.py",
"register_output_renderer.py",
"view_name.py",
)
{
"name": "messages_output_renderer.py",
"static": False,
"templates": False,
"version": None,
"hooks": ["register_output_renderer"],
},
{
"name": "my_plugin.py",
"static": False,
"templates": False,
"version": None,
"hooks": [
"actor_from_request",
"extra_body_script",
"extra_css_urls",
"extra_js_urls",
"extra_template_vars",
"permission_allowed",
"prepare_connection",
"prepare_jinja2_environment",
"register_facet_classes",
"render_cell",
],
},
{
"name": "my_plugin_2.py",
"static": False,
"templates": False,
"version": None,
"hooks": [
"actor_from_request",
"asgi_wrapper",
"extra_js_urls",
"extra_template_vars",
"permission_allowed",
"render_cell",
],
},
{
"name": "register_output_renderer.py",
"static": False,
"templates": False,
"version": None,
"hooks": ["register_output_renderer"],
},
{
"name": "view_name.py",
"static": False,
"templates": False,
"version": None,
"hooks": ["extra_template_vars"],
},
]
assert expected == sorted(response.json, key=lambda p: p["name"])
@ -1761,16 +1807,10 @@ def test_common_prefix_database_names(app_client_conflicting_database_names):
# https://github.com/simonw/datasette/issues/597
assert ["fixtures", "foo", "foo-bar"] == [
d["name"]
for d in json.loads(
app_client_conflicting_database_names.get("/-/databases.json").body.decode(
"utf8"
)
)
for d in app_client_conflicting_database_names.get("/-/databases.json").json
]
for db_name, path in (("foo", "/foo.json"), ("foo-bar", "/foo-bar.json")):
data = json.loads(
app_client_conflicting_database_names.get(path).body.decode("utf8")
)
data = app_client_conflicting_database_names.get(path).json
assert db_name == data["database"]

View file

@ -9,11 +9,7 @@ def test_auth_token(app_client):
response = app_client.get(path, allow_redirects=False,)
assert 302 == response.status
assert "/" == response.headers["Location"]
set_cookie = response.headers["set-cookie"]
assert set_cookie.endswith("; Path=/")
assert set_cookie.startswith("ds_actor=")
cookie_value = set_cookie.split("ds_actor=")[1].split("; Path=/")[0]
assert {"id": "root"} == app_client.ds.unsign(cookie_value, "actor")
assert {"id": "root"} == app_client.ds.unsign(response.cookies["ds_actor"], "actor")
# Check that a second with same token fails
assert app_client.ds._root_token is None
assert 403 == app_client.get(path, allow_redirects=False,).status

View file

@ -84,21 +84,20 @@ def config_dir_client(tmp_path_factory):
def test_metadata(config_dir_client):
response = config_dir_client.get("/-/metadata.json")
assert 200 == response.status
assert METADATA == json.loads(response.text)
assert METADATA == response.json
def test_config(config_dir_client):
response = config_dir_client.get("/-/config.json")
assert 200 == response.status
config = json.loads(response.text)
assert 60 == config["default_cache_ttl"]
assert not config["allow_sql"]
assert 60 == response.json["default_cache_ttl"]
assert not response.json["allow_sql"]
def test_plugins(config_dir_client):
response = config_dir_client.get("/-/plugins.json")
assert 200 == response.status
assert "hooray.py" in {p["name"] for p in json.loads(response.text)}
assert "hooray.py" in {p["name"] for p in response.json}
def test_templates_and_plugin(config_dir_client):
@ -123,7 +122,7 @@ def test_static_directory_browsing_not_allowed(config_dir_client):
def test_databases(config_dir_client):
response = config_dir_client.get("/-/databases.json")
assert 200 == response.status
databases = json.loads(response.text)
databases = response.json
assert 2 == len(databases)
databases.sort(key=lambda d: d["name"])
assert "demo" == databases[0]["name"]
@ -141,4 +140,4 @@ def test_metadata_yaml(tmp_path_factory, filename):
client.ds = ds
response = client.get("/-/metadata.json")
assert 200 == response.status
assert {"title": "Title from metadata"} == json.loads(response.text)
assert {"title": "Title from metadata"} == response.json

View file

@ -101,7 +101,7 @@ def test_csv_with_non_ascii_characters(app_client):
)
assert response.status == 200
assert "text/plain; charset=utf-8" == response.headers["content-type"]
assert "text,number\r\n𝐜𝐢𝐭𝐢𝐞𝐬,1\r\nbob,2\r\n" == response.body.decode("utf8")
assert "text,number\r\n𝐜𝐢𝐭𝐢𝐞𝐬,1\r\nbob,2\r\n" == response.text
def test_max_csv_mb(app_client_csv_max_mb_one):

View file

@ -606,9 +606,7 @@ def test_row_html_simple_primary_key(app_client):
def test_table_not_exists(app_client):
assert "Table not found: blah" in app_client.get("/fixtures/blah").body.decode(
"utf8"
)
assert "Table not found: blah" in app_client.get("/fixtures/blah").text
def test_table_html_no_primary_key(app_client):

28
tests/test_messages.py Normal file
View file

@ -0,0 +1,28 @@
from .fixtures import app_client
import pytest
@pytest.mark.parametrize(
"qs,expected",
[
("add_msg=added-message", [["added-message", 1]]),
("add_msg=added-warning&type=WARNING", [["added-warning", 2]]),
("add_msg=added-error&type=ERROR", [["added-error", 3]]),
],
)
def test_add_message_sets_cookie(app_client, qs, expected):
response = app_client.get("/fixtures.message?{}".format(qs))
signed = response.cookies["ds_messages"]
decoded = app_client.ds.unsign(signed, "messages")
assert expected == decoded
def test_messages_are_displayed_and_cleared(app_client):
# First set the message cookie
set_msg_response = app_client.get("/fixtures.message?add_msg=xmessagex")
# Now access a page that displays messages
response = app_client.get("/", cookies=set_msg_response.cookies)
# Messages should be in that HTML
assert "xmessagex" in response.text
# Cookie should have been set that clears messages
assert "" == response.cookies["ds_messages"]

View file

@ -218,7 +218,7 @@ def test_plugin_config_file(app_client):
)
def test_plugins_extra_body_script(app_client, path, expected_extra_body_script):
r = re.compile(r"<script>var extra_body_script = (.*?);</script>")
json_data = r.search(app_client.get(path).body.decode("utf8")).group(1)
json_data = r.search(app_client.get(path).text).group(1)
actual_data = json.loads(json_data)
assert expected_extra_body_script == actual_data
@ -331,7 +331,7 @@ def view_names_client(tmp_path_factory):
def test_view_names(view_names_client, path, view_name):
response = view_names_client.get(path)
assert response.status == 200
assert "view_name:{}".format(view_name) == response.body.decode("utf8")
assert "view_name:{}".format(view_name) == response.text
def test_register_output_renderer_no_parameters(app_client):
@ -345,8 +345,7 @@ def test_register_output_renderer_all_parameters(app_client):
assert 200 == response.status
# Lots of 'at 0x103a4a690' in here - replace those so we can do
# an easy comparison
body = response.body.decode("utf-8")
body = at_memory_re.sub(" at 0xXXX", body)
body = at_memory_re.sub(" at 0xXXX", response.text)
assert {
"1+1": 2,
"datasette": "<datasette.app.Datasette object at 0xXXX>",
@ -468,7 +467,6 @@ def test_register_facet_classes(app_client):
response = app_client.get(
"/fixtures/compound_three_primary_keys.json?_dummy_facet=1"
)
data = json.loads(response.body)
assert [
{
"name": "pk1",
@ -502,7 +500,7 @@ def test_register_facet_classes(app_client):
"name": "pk3",
"toggle_url": "http://localhost/fixtures/compound_three_primary_keys.json?_dummy_facet=1&_facet=pk3",
},
] == data["suggested_facets"]
] == response.json["suggested_facets"]
def test_actor_from_request(app_client):