From d97e82df3c8a3f2e97038d7080167be9bb74a68d Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 22 Mar 2023 15:49:39 -0700 Subject: [PATCH] ?_extra= support and TableView refactor to table_view * Implemented ?_extra= option for JSON views, refs #262 * New dependency: asyncinject * Remove now-obsolete TableView class --- datasette/app.py | 22 +- datasette/cli.py | 1 + datasette/renderer.py | 20 +- .../_description_source_license.html | 4 +- datasette/templates/_suggested_facets.html | 2 +- datasette/templates/base.html | 4 +- datasette/templates/table.html | 6 +- datasette/utils/__init__.py | 13 +- datasette/views/base.py | 338 ++- datasette/views/database.py | 1 + datasette/views/table.py | 2011 ++++++++++------- setup.py | 1 + tests/test_api.py | 10 +- tests/test_facets.py | 30 +- tests/test_filters.py | 46 +- tests/test_load_extensions.py | 3 +- tests/test_plugins.py | 20 +- tests/test_routes.py | 16 +- tests/test_table_api.py | 131 +- tests/test_table_html.py | 7 + 20 files changed, 1597 insertions(+), 1089 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index df33386b..d7dace67 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -1,5 +1,4 @@ import asyncio -from pydoc import plain from typing import Sequence, Union, Tuple, Optional, Dict, Iterable import asgi_csrf import collections @@ -24,7 +23,12 @@ from pathlib import Path from markupsafe import Markup, escape from itsdangerous import URLSafeSerializer -from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader +from jinja2 import ( + ChoiceLoader, + Environment, + FileSystemLoader, + PrefixLoader, +) from jinja2.environment import Template from jinja2.exceptions import TemplateNotFound @@ -42,7 +46,12 @@ from .views.special import ( PermissionsDebugView, MessagesDebugView, ) -from .views.table import TableView, TableInsertView, TableUpsertView, TableDropView +from .views.table import ( + TableInsertView, + TableUpsertView, + TableDropView, + table_view, +) from .views.row import RowView, RowDeleteView, RowUpdateView from .renderer import json_renderer from .url_builder import Urls @@ -389,7 +398,10 @@ class Datasette: ] ) self.jinja_env = Environment( - loader=template_loader, autoescape=True, enable_async=True + loader=template_loader, + autoescape=True, + enable_async=True, + # undefined=StrictUndefined, ) self.jinja_env.filters["escape_css_string"] = escape_css_string self.jinja_env.filters["quote_plus"] = urllib.parse.quote_plus @@ -1358,7 +1370,7 @@ class Datasette: ) add_route(TableCreateView.as_view(self), r"/(?P[^\/\.]+)/-/create$") add_route( - TableView.as_view(self), + wrap_view(table_view, self), r"/(?P[^\/\.]+)/(?P[^\/\.]+)(\.(?P\w+))?$", ) add_route( diff --git a/datasette/cli.py b/datasette/cli.py index a3ae1269..a6de9e6d 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -136,6 +136,7 @@ def sqlite_extensions(fn): multiple=True, help="Path to a SQLite extension to load, and optional entrypoint", )(fn) + # Wrap it in a custom error handler @functools.wraps(fn) def wrapped(*args, **kwargs): diff --git a/datasette/renderer.py b/datasette/renderer.py index 16990efa..5354f348 100644 --- a/datasette/renderer.py +++ b/datasette/renderer.py @@ -4,6 +4,7 @@ from datasette.utils import ( remove_infinites, CustomJSONEncoder, path_from_row_pks, + sqlite3, ) from datasette.utils.asgi import Response @@ -49,10 +50,14 @@ def json_renderer(args, data, view_name): if data.get("error"): shape = "objects" - next_url = data.get("next_url") - if shape == "arrayfirst": - data = [row[0] for row in data["rows"]] + if not data["rows"]: + data = [] + elif isinstance(data["rows"][0], sqlite3.Row): + data = [row[0] for row in data["rows"]] + else: + assert isinstance(data["rows"][0], dict) + data = [next(iter(row.values())) for row in data["rows"]] elif shape in ("objects", "object", "array"): columns = data.get("columns") rows = data.get("rows") @@ -80,7 +85,12 @@ def json_renderer(args, data, view_name): data = data["rows"] elif shape == "arrays": - pass + if not data["rows"]: + pass + elif isinstance(data["rows"][0], sqlite3.Row): + data["rows"] = [list(row) for row in data["rows"]] + else: + data["rows"] = [list(row.values()) for row in data["rows"]] else: status_code = 400 data = { @@ -98,8 +108,6 @@ def json_renderer(args, data, view_name): body = json.dumps(data, cls=CustomJSONEncoder) content_type = "application/json; charset=utf-8" headers = {} - if next_url: - headers["link"] = f'<{next_url}>; rel="next"' return Response( body, status=status_code, headers=headers, content_type=content_type ) diff --git a/datasette/templates/_description_source_license.html b/datasette/templates/_description_source_license.html index a2bc18f2..f852268f 100644 --- a/datasette/templates/_description_source_license.html +++ b/datasette/templates/_description_source_license.html @@ -1,6 +1,6 @@ -{% if metadata.description_html or metadata.description %} +{% if metadata.get("description_html") or metadata.get("description") %}