Compare commits

..

2 Commits

Author SHA1 Message Date
evilchili
120449386a unique constraint uses equals not matches 2025-10-03 16:44:25 -07:00
evilchili
8ce642ba90 WIP 2025-09-28 14:14:30 -07:00
6 changed files with 199 additions and 29 deletions

View File

@ -44,6 +44,8 @@ ADMIN_EMAIL=
THEME=default THEME=default
VIEW_URI=/
""" """
def __init__(self): def __init__(self):
@ -86,12 +88,16 @@ THEME=default
if self.config.IN_MEMORY_DB: if self.config.IN_MEMORY_DB:
self.db = GrungDB.with_schema(schema, storage=MemoryStorage) self.db = GrungDB.with_schema(schema, storage=MemoryStorage)
else: else:
self.db = GrungDB.with_schema(schema, self.path.database) self.db = GrungDB.with_schema(
schema, self.path.database, sort_keys=True, indent=4, separators=(",", ": ")
)
self.theme = Path(__file__).parent / 'themes' / "default" self.theme = Path(__file__).parent / "themes" / "default"
self.web = Flask(self.config.NAME, template_folder=self.theme) self.web = Flask(self.config.NAME, template_folder=self.theme)
self.web.config["SECRET_KEY"] = self.config.SECRET_KEY self.web.config["SECRET_KEY"] = self.config.SECRET_KEY
self.web.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
self.web.config["DEBUG"] = True
self._initialized = True self._initialized = True
@ -99,15 +105,35 @@ THEME=default
if not self._initialized: if not self._initialized:
raise ApplicationNotInitializedError("This action requires the application to be initialized.") raise ApplicationNotInitializedError("This action requires the application to be initialized.")
def add_page(self, parent: schema.Page, child: schema.Page):
parent.pages.append(self.db.save(child))
parent = self.db.save(parent)
return parent.get_child(child)
def bootstrap(self): def bootstrap(self):
""" """
Bootstrap the database entries by populating the first Page, the Admin user and the Admins group. Bootstrap the database entries by populating the first Page, the Admin user and the Admins group.
""" """
self.check_state() self.check_state()
home = schema.Page(stub=self.config.VIEW_URI, title="Home", body="This is the home page")
npcs = schema.Page(stub="NPC", title="NPC", body="NPCs!")
sabetha = schema.Page(title="Sabetha", body="Sabetha!")
try: try:
self.db.save(schema.Page(parent_id=None, stub="", title="_", body="")) home = self.db.save(home)
except UniqueConstraintError: except UniqueConstraintError:
pass pass
try:
npcs = self.add_page(home, npcs)
except UniqueConstraintError:
pass
try:
sabetha = self.add_page(npcs, sabetha)
except UniqueConstraintError:
pass
try: try:
admin = self.db.save(schema.User(name=self.config.ADMIN_USERNAME, email=self.config.ADMIN_EMAIL)) admin = self.db.save(schema.User(name=self.config.ADMIN_USERNAME, email=self.config.ADMIN_EMAIL))
except UniqueConstraintError: except UniqueConstraintError:

View File

@ -53,6 +53,7 @@ def init(context: typer.Context, drop: bool = typer.Option(False, help="Drop tab
ttfrog.app.db.close() ttfrog.app.db.close()
ttfrog.app.initialize(force=True) ttfrog.app.initialize(force=True)
ttfrog.app.bootstrap() ttfrog.app.bootstrap()
print(ttfrog.app.db.Page.all())
print(ttfrog.app.db) print(ttfrog.app.db)
@ -62,6 +63,7 @@ def run(context: typer.Context):
The default CLI entrypoint is ttfrog.cli.run(). The default CLI entrypoint is ttfrog.cli.run().
""" """
import ttfrog.web import ttfrog.web
ttfrog.app.web.run() ttfrog.app.web.run()

View File

@ -1,18 +1,52 @@
from grung.types import Collection, Field, Record from grung.types import BackReference, Collection, Field, Record
class User(Record): class User(Record):
_fields = [Field("name"), Field("email", unique=True)] @classmethod
def fields(cls):
return [*super().fields(), Field("name"), Field("email", unique=True)]
class Group(Record): class Group(Record):
_fields = [Field("name", unique=True), Collection("users", User)] @classmethod
def fields(cls):
return [*super().fields(), Field("name", unique=True), Collection("users", User)]
class Page(Record): class Page(Record):
_fields = [ @classmethod
Field("parent_id"), def fields(cls):
Field("stub"), return [
Field("title"), *super().fields(),
Field("body"), Field("uri", unique=True),
] Field("stub"),
Field("title"),
Field("body"),
Collection("pages", Page),
BackReference("parent", value_type=Page),
]
def before_insert(self, db):
super().before_insert(db)
if not self.stub and not self.title:
raise Exception("Must provide either a stub or a title!")
if not self.stub:
self.stub = self.title.title().replace(" ", "")
if not self.title:
self.title = self.stub
self.uri = (self.parent.uri + "/" if self.parent and self.parent.uri != "/" else "") + self.stub
def after_insert(self, db):
super().after_insert(db)
for child in self.pages:
obj = BackReference.dereference(child, db)
obj.uri = f"{self.uri}/{obj.stub}"
child = db.save(obj)
def get_child(self, obj: Record):
for page in self.pages:
if page.uid == obj.uid:
return page
return None

View File

@ -12,8 +12,14 @@
</head> </head>
<body> <body>
<nav> <nav>
{% for uri, stub in breadcrumbs %}
<span>&nbsp; / <a href="{{ app.config.VIEW_URI }}{{ uri }}">{{ stub }}</a></span>
{% endfor %}
</nav>
<nav>
Menu:
<ul> <ul>
<li><a href="{{ url_for('index') }}">Home</a></li> {% block menu %}{% endblock %}
</ul> </ul>
</nav> </nav>

View File

@ -1,6 +1,26 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block menu %}
<h1>{{ page.title }}</h1> {% for child in page.pages %}
{{ page.content }} <li><a href="{{ app.config.VIEW_URI }}{{ child.uri }}">{{ child.title }}</a></li>
{% endfor %}
{% endblock %} {% endblock %}
{% block content %}
<form method='POST'>
<input type="hidden" name="uid" value="{{ page.uid }}">
<h1>{{ page.doc_id }}: <input name='title' type='text' value="{{ page.title }}"></h1>
<h3>{{ app.config.VIEW_URI }}{{ page.parent.uri if page.parent else "/" }} <input name='stub' type='text' value='{{ page.stub }}'></h3>
<textarea name='body'>{{ page.body }}</textarea>
<input type=submit>
</form>
<pre>
{{ page }}
</pre>
<pre>
{{ app.web.config }}
</pre>
{% endblock %}

View File

@ -1,22 +1,104 @@
from ttfrog import app from flask import Response, render_template, request
from flask import Response, render_template
from tinydb import where from tinydb import where
import logging
from ttfrog import app, schema
from ttfrog.forms import PageForm
STATIC = ["static"]
logger = logging.getLogger(__name__) def relative_uri(path: str = ""):
"""
The request's URI relative to the VIEW_URI without the leading '/'.
"""
return (path or request.path).replace(app.config.VIEW_URI, "", 1).strip("/") or "/"
def get_parent(uri: str):
try:
parent_uri = uri.strip("/").split("/", -1)[0]
except IndexError:
return None
app.web.logger.debug(f"Looking for parent with {parent_uri = }")
return get_page(parent_uri or "/", create_okay=False)
def get_page(path: str = "", create_okay: bool = False):
"""
Get one page, including its subpages, but not recursively.
"""
uri = path or relative_uri(request.path)
matches = app.db.Page.search(where("uri") == uri, recurse=False)
app.web.logger.debug(f"Found {len(matches)} pages where {uri = }")
if not matches:
if not create_okay:
return None
return schema.Page(stub=uri.split("/")[-1], body="This page does not exist", parent=get_parent(uri))
uids = [pointer.split("::")[-1] for pointer in matches[0].pages]
subpages = app.db.Page.search(where("uid").one_of(uids), recurse=False)
matches[0].pages = subpages
return matches[0]
def rendered(page: schema.Record, template: str = "page.html"):
if not page:
return Response("Page not found", status=404)
return render_template(template, page=page, app=app, breadcrumbs=breadcrumbs())
def get_static(path):
return Response("OK", status=200)
def breadcrumbs():
"""
Return (uri, stub) pairs for the parents leading from the VIEW_URI to the current request.
"""
if app.config.VIEW_URI != "/":
root = get_page()
yield (app.config.VIEW_URI, root.stub)
uri = ""
for stub in relative_uri().split("/"):
uri = "/".join([uri, stub]).lstrip("/")
yield (uri, stub)
@app.web.route("/") @app.web.route("/")
def index(): def index():
page = app.db.Page.search(where('stub') == "") return rendered(get_page(create_okay=False))
return render_template("page.html", page=page[0])
@app.web.route("/<stub>") @app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["GET"])
def page_view(stub): def view(path):
page = app.db.Page.search(where('stub') == stub) return rendered(get_page(request.path, create_okay=True))
if not page:
logger.info(f"No page found for {stub = }")
return Response(f"{stub}: not found", status=404) @app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["POST"])
return render_template("page.html", page=page[0]) def edit(path):
uri = relative_uri()
parent = get_parent(uri)
app.web.logger.debug(f"Handling form submission: {uri = }, {parent = }, {request.form = }")
if not parent:
return Response(f"Parent for {uri} does not exist.", status=403)
page = get_page(uri, create_okay=True)
app.web.logger.debug(f"Editing {page.doc_id} for {uri = }")
if page.doc_id:
if page.uid != request.form["uid"]:
return Response("Invalid UID.", status=403)
form = PageForm(page, request.form)
page = app.db.save(form.prepare())
app.web.logger.debug(f"Saved {page.doc_id}; now updating parent {parent.doc_id}")
parent.pages.append(page)
app.db.save(parent)
return get_page(stub)
@app.web.after_request
def add_header(r):
r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, public, max-age=0"
r.headers["Pragma"] = "no-cache"
r.headers["Expires"] = "0"
return r