datasette-edit-templates/datasette_edit_templates/__init__.py
from datasette import hookimplfrom jinja2 import FunctionLoader, TemplateNotFoundfrom datasette.utils.asgi import Response, Forbiddenimport datetimeTABLE = "_templates_"CREATE_TABLE_SQLS = ["""CREATE TABLE {table} ([id] INTEGER PRIMARY KEY,[template] TEXT,[created] TEXT,[body] TEXT);""".format(table=TABLE),"""CREATE INDEX idx_template_created ON {table}(template, created);""".format(table=TABLE),]LOAD_TEMPLATE_SQL = """select template, bodyfrom {table}group by templatehaving created = max(created)""".format(table=TABLE)GET_TEMPLATE_SQL = "SELECT id, body FROM {} WHERE template = :template ORDER BY created DESC LIMIT 1".format(TABLE)WRITE_TEMPLATE_SQL = "INSERT INTO {} (template, created, body) VALUES (:template, :created, :body)".format(TABLE)def pretty_datetime(d):if d:return datetime.datetime.fromisoformat(d).strftime("%Y-%m-%d %H:%M:%S")else:return ""def get_environment(datasette, request):if hasattr(datasette, "get_jinja_environment"):return datasette.get_jinja_environment(request)return datasette.jinja_env@hookimpldef startup(datasette):datasette._edit_templates = {}async def inner():db = get_database(datasette)# Does the table exist?if not await db.table_exists(TABLE):for sql in CREATE_TABLE_SQLS:await db.execute_write(sql, block=True)else:# Load all templates from that tablerows = await db.execute(LOAD_TEMPLATE_SQL)for name, content in rows:datasette._edit_templates[name] = contentreturn inner@hookimpldef permission_allowed(actor, action):if action == "edit-templates" and actor and actor.get("id") == "root":return True@hookimpldef menu_links(datasette, actor):config = datasette.plugin_config("datasette-edit-templates") or {}if "menu_label" not in config:menu_label = "Edit templates"else:menu_label = config.get("menu_label")if menu_label is None:returnasync def inner():if not await datasette.permission_allowed(actor, "edit-templates", default=False):returnreturn [{"href": datasette.urls.path("/-/edit-templates"),"label": menu_label,},]return innerdef get_database(datasette):plugin_config = datasette.plugin_config("datasette-edit-templates") or {}if plugin_config.get("internal_db"):return datasette.get_internal_database()return datasette.get_database(plugin_config.get("database"))class MyFunctionLoader(FunctionLoader):def list_templates(self):return []@hookimpldef prepare_jinja2_environment(env, datasette):config = datasette.plugin_config("datasette-edit-templates") or {}if config.get("skip_prepare_jinja2_environment"):returndef load_func(path):try:code = datasette._edit_templates[path]return code, path, lambda: Trueexcept KeyError:return Noneenv.loader.loaders.insert(0, MyFunctionLoader(load_func))async def edit_templates_index(request, datasette):if not await datasette.permission_allowed(request.actor, "edit-templates", default=False):raise Forbidden("Permission denied for edit-templates")template_name = request.args.get("template")if template_name:return Response.redirect(datasette.urls.path("/-/edit-templates/{}".format(template_name)))db = get_database(datasette)# List both templates from DB and default unedited templatestemplates = [dict(row)for row in await db.execute("""select template as name, max(created) as last_updated, count(*) as revisionsfrom {} group by template order by created desc""".format(TABLE))]by_name = {t["name"]: t for t in templates}environment = get_environment(datasette, request)for template in environment.list_templates():if template.startswith("default:"):continueif template in by_name:passelse:templates.append({"name": template,"last_edited": None,"revisions": "",})# Offer edit options for all disk templatesreturn Response.html(await datasette.render_template("edit_templates_index.html",{"templates": templates,"pretty_datetime": pretty_datetime,},))async def edit_template(request, datasette):if not await datasette.permission_allowed(request.actor, "edit-templates", default=False):raise Forbidden("Permission denied for edit-templates")template = request.url_vars["template"]db = get_database(datasette)if request.method == "POST":post = await request.post_vars()body = post["body"]await db.execute_write(WRITE_TEMPLATE_SQL,{"template": template,"created": datetime.datetime.utcnow().isoformat(),"body": body,},block=True,)datasette.add_message(request, "Template changes saved")# Update the in-memory cache toodatasette._edit_templates[template] = body# Clear the Jinja template cache so it sees the changeget_environment(datasette, request).cache.clear()return Response.redirect(request.path)create_from_scratch = Falserow = (await db.execute(GET_TEMPLATE_SQL, {"template": template})).first()requested_revision = request.args.get("revision")revision = Noneif requested_revision:row = (await db.execute("select id, created, body from {} where id = :id".format(TABLE),{"id": requested_revision},)).first()revision = dict(row)revisions = []if row is None:from_db = False# Load it from disk insteadtry:environment = get_environment(datasette, request)template_obj = environment.get_template(template)body = open(template_obj.filename).read()except TemplateNotFound:body = ""create_from_scratch = Trueelse:from_db = Truebody = row["body"]# And load revisionsrevisions = [dict(revision)for revision in (await db.execute("select id, created from {} where template = :template and id != :id order by created desc".format(TABLE),{"template": template, "id": row["id"]},)).rows]return Response.html(await datasette.render_template("edit_template.html",{"body": body,"template": template,"revision": revision,"path": request.path,"from_db": from_db,"create_from_scratch": create_from_scratch,"revisions": revisions,"pretty_datetime": pretty_datetime,},request=request,))@hookimpldef register_routes():return [(r"^/-/edit-templates$", edit_templates_index),(r"^/-/edit-templates/(?P<template>.+)$", edit_template),]