New View base class (#2080)

* New View base class, closes #2078
* Use new View subclass for PatternPortfolioView
pull/2082/head
Simon Willison 2023-05-25 17:18:43 -07:00 zatwierdzone przez GitHub
rodzic b49fa446d6
commit dda99fc09f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
4 zmienionych plików z 170 dodań i 10 usunięć

Wyświetl plik

@ -17,6 +17,7 @@ import secrets
import sys import sys
import threading import threading
import time import time
import types
import urllib.parse import urllib.parse
from concurrent import futures from concurrent import futures
from pathlib import Path from pathlib import Path
@ -1361,7 +1362,7 @@ class Datasette:
r"/-/allow-debug$", r"/-/allow-debug$",
) )
add_route( add_route(
PatternPortfolioView.as_view(self), wrap_view(PatternPortfolioView, self),
r"/-/patterns$", r"/-/patterns$",
) )
add_route(DatabaseDownload.as_view(self), r"/(?P<database>[^\/\.]+)\.db$") add_route(DatabaseDownload.as_view(self), r"/(?P<database>[^\/\.]+)\.db$")
@ -1673,7 +1674,42 @@ def _cleaner_task_str(task):
return _cleaner_task_str_re.sub("", s) return _cleaner_task_str_re.sub("", s)
def wrap_view(view_fn, datasette): def wrap_view(view_fn_or_class, datasette):
is_function = isinstance(view_fn_or_class, types.FunctionType)
if is_function:
return wrap_view_function(view_fn_or_class, datasette)
else:
if not isinstance(view_fn_or_class, type):
raise ValueError("view_fn_or_class must be a function or a class")
return wrap_view_class(view_fn_or_class, datasette)
def wrap_view_class(view_class, datasette):
async def async_view_for_class(request, send):
instance = view_class()
if inspect.iscoroutinefunction(instance.__call__):
return await async_call_with_supported_arguments(
instance.__call__,
scope=request.scope,
receive=request.receive,
send=send,
request=request,
datasette=datasette,
)
else:
return call_with_supported_arguments(
instance.__call__,
scope=request.scope,
receive=request.receive,
send=send,
request=request,
datasette=datasette,
)
return async_view_for_class
def wrap_view_function(view_fn, datasette):
@functools.wraps(view_fn) @functools.wraps(view_fn)
async def async_view_fn(request, send): async def async_view_fn(request, send):
if inspect.iscoroutinefunction(view_fn): if inspect.iscoroutinefunction(view_fn):

Wyświetl plik

@ -53,6 +53,43 @@ class DatasetteError(Exception):
self.message_is_html = message_is_html self.message_is_html = message_is_html
class View:
async def head(self, request, datasette):
if not hasattr(self, "get"):
return await self.method_not_allowed(request)
response = await self.get(request, datasette)
response.body = ""
return response
async def method_not_allowed(self, request):
if (
request.path.endswith(".json")
or request.headers.get("content-type") == "application/json"
):
response = Response.json(
{"ok": False, "error": "Method not allowed"}, status=405
)
else:
response = Response.text("Method not allowed", status=405)
return response
async def options(self, request, datasette):
response = Response.text("ok")
response.headers["allow"] = ", ".join(
method.upper()
for method in ("head", "get", "post", "put", "patch", "delete")
if hasattr(self, method)
)
return response
async def __call__(self, request, datasette):
try:
handler = getattr(self, request.method.lower())
except AttributeError:
return await self.method_not_allowed(request)
return await handler(request, datasette)
class BaseView: class BaseView:
ds = None ds = None
has_json_alternate = True has_json_alternate = True

Wyświetl plik

@ -6,7 +6,7 @@ from datasette.utils import (
tilde_encode, tilde_encode,
tilde_decode, tilde_decode,
) )
from .base import BaseView from .base import BaseView, View
import secrets import secrets
import urllib import urllib
@ -57,13 +57,16 @@ class JsonDataView(BaseView):
) )
class PatternPortfolioView(BaseView): class PatternPortfolioView(View):
name = "patterns" async def get(self, request, datasette):
has_json_alternate = False await datasette.ensure_permissions(request.actor, ["view-instance"])
return Response.html(
async def get(self, request): await datasette.render_template(
await self.ds.ensure_permissions(request.actor, ["view-instance"]) "patterns.html",
return await self.render(["patterns.html"], request=request) request=request,
view_name="patterns",
)
)
class AuthTokenView(BaseView): class AuthTokenView(BaseView):

Wyświetl plik

@ -0,0 +1,84 @@
from datasette.views.base import View
from datasette import Request, Response
from datasette.app import Datasette
import json
import pytest
class GetView(View):
async def get(self, request, datasette):
return Response.json(
{
"absolute_url": datasette.absolute_url(request, "/"),
"request_path": request.path,
}
)
class GetAndPostView(GetView):
async def post(self, request, datasette):
return Response.json(
{
"method": request.method,
"absolute_url": datasette.absolute_url(request, "/"),
"request_path": request.path,
}
)
@pytest.mark.asyncio
async def test_get_view():
v = GetView()
datasette = Datasette()
response = await v(Request.fake("/foo"), datasette)
assert json.loads(response.body) == {
"absolute_url": "http://localhost/",
"request_path": "/foo",
}
# Try a HEAD request
head_response = await v(Request.fake("/foo", method="HEAD"), datasette)
assert head_response.body == ""
assert head_response.status == 200
# And OPTIONS
options_response = await v(Request.fake("/foo", method="OPTIONS"), datasette)
assert options_response.body == "ok"
assert options_response.status == 200
assert options_response.headers["allow"] == "HEAD, GET"
# And POST
post_response = await v(Request.fake("/foo", method="POST"), datasette)
assert post_response.body == "Method not allowed"
assert post_response.status == 405
# And POST with .json extension
post_json_response = await v(Request.fake("/foo.json", method="POST"), datasette)
assert json.loads(post_json_response.body) == {
"ok": False,
"error": "Method not allowed",
}
assert post_json_response.status == 405
@pytest.mark.asyncio
async def test_post_view():
v = GetAndPostView()
datasette = Datasette()
response = await v(Request.fake("/foo"), datasette)
assert json.loads(response.body) == {
"absolute_url": "http://localhost/",
"request_path": "/foo",
}
# Try a HEAD request
head_response = await v(Request.fake("/foo", method="HEAD"), datasette)
assert head_response.body == ""
assert head_response.status == 200
# And OPTIONS
options_response = await v(Request.fake("/foo", method="OPTIONS"), datasette)
assert options_response.body == "ok"
assert options_response.status == 200
assert options_response.headers["allow"] == "HEAD, GET, POST"
# And POST
post_response = await v(Request.fake("/foo", method="POST"), datasette)
assert json.loads(post_response.body) == {
"method": "POST",
"absolute_url": "http://localhost/",
"request_path": "/foo",
}