unique constraint uses equals not matches

This commit is contained in:
evilchili 2025-10-03 16:44:25 -07:00
parent 8ce642ba90
commit 120449386a
5 changed files with 137 additions and 46 deletions

View File

@ -44,6 +44,8 @@ ADMIN_EMAIL=
THEME=default THEME=default
VIEW_URI=/
""" """
def __init__(self): def __init__(self):
@ -87,18 +89,14 @@ THEME=default
self.db = GrungDB.with_schema(schema, storage=MemoryStorage) self.db = GrungDB.with_schema(schema, storage=MemoryStorage)
else: else:
self.db = GrungDB.with_schema( self.db = GrungDB.with_schema(
schema, schema, self.path.database, sort_keys=True, indent=4, separators=(",", ": ")
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["SEND_FILE_MAX_AGE_DEFAULT"] = 0
self.web.config["DEBUG"] = True self.web.config["DEBUG"] = True
self._initialized = True self._initialized = True
@ -107,20 +105,32 @@ 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:
about = self.db.save(schema.Page(title="About", body="About!")) home = self.db.save(home)
except UniqueConstraintError: except UniqueConstraintError:
pass pass
try: try:
home = self.db.save(schema.Page(title="Home", body="This is the home page", pages=[about])) npcs = self.add_page(home, npcs)
about.parent = home except UniqueConstraintError:
self.db.ssave(home) pass
try:
sabetha = self.add_page(npcs, sabetha)
except UniqueConstraintError: except UniqueConstraintError:
pass pass

View File

@ -1,4 +1,4 @@
from grung.types import Collection, Field, Record, Pointer from grung.types import BackReference, Collection, Field, Record
class User(Record): class User(Record):
@ -18,17 +18,35 @@ class Page(Record):
def fields(cls): def fields(cls):
return [ return [
*super().fields(), *super().fields(),
Field("stub", unique=True), Field("uri", unique=True),
Field("stub"),
Field("title"), Field("title"),
Field("body"), Field("body"),
Pointer("parent", value_type=Page),
Collection("pages", Page), Collection("pages", Page),
BackReference("parent", value_type=Page),
] ]
def before_insert(self): def before_insert(self, db):
super().before_insert(db)
if not self.stub and not self.title: if not self.stub and not self.title:
raise Exception("Must provide either a stub or a title!") raise Exception("Must provide either a stub or a title!")
if not self.stub: if not self.stub:
self.stub = self.title.title().replace(" ", "") self.stub = self.title.title().replace(" ", "")
if not self.title: if not self.title:
self.title = self.stub.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,11 +12,12 @@
</head> </head>
<body> <body>
<nav> <nav>
<ul> {% for uri, stub in breadcrumbs %}
<li><a href="{{ url_for('index') }}">Home</a></li> <span>&nbsp; / <a href="{{ app.config.VIEW_URI }}{{ uri }}">{{ stub }}</a></span>
</ul> {% endfor %}
</nav> </nav>
<nav> <nav>
Menu:
<ul> <ul>
{% block menu %}{% endblock %} {% block menu %}{% endblock %}
</ul> </ul>

View File

@ -2,16 +2,25 @@
{% block menu %} {% block menu %}
{% for child in page.pages %} {% for child in page.pages %}
<li><a href="/{{page.stub}}/{{ child.stub }}">{{ child.title }}</a></li> <li><a href="{{ app.config.VIEW_URI }}{{ child.uri }}">{{ child.title }}</a></li>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>{{ page.title }}</h1> <form method='POST'>
{{ page.body }} <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> <pre>
{{ page }} {{ page }}
</pre> </pre>
<pre>
{{ app.web.config }}
</pre>
{% endblock %} {% endblock %}

View File

@ -1,46 +1,99 @@
from flask import Response, render_template from flask import Response, render_template, request
from tinydb import where from tinydb import where
from ttfrog import app from ttfrog import app, schema
from ttfrog.forms import PageForm
STATIC = ["static"]
def get_page(stub): 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. Get one page, including its subpages, but not recursively.
""" """
app.web.logger.debug(f"Looking for page with {stub = }") uri = path or relative_uri(request.path)
matches = app.db.Page.search(where("stub") == stub, recurse=False) 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 matches:
return 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] uids = [pointer.split("::")[-1] for pointer in matches[0].pages]
subpages = app.db.Page.search(where("uid").one_of(uids), recurse=False) subpages = app.db.Page.search(where("uid").one_of(uids), recurse=False)
matches[0].pages = subpages matches[0].pages = subpages
return matches[0] 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 = get_page("Home") return rendered(get_page(create_okay=False))
if not page:
return Response("Home page not found", status=404)
return render_template("page.html", page=page)
@app.web.route("/view/<path:path>") @app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["GET"])
def page_view(path): def view(path):
path = path.rstrip("/") return rendered(get_page(request.path, create_okay=True))
stub = path.split("/")[-1]
page = get_page(stub)
app.web.logger.debug(f"page_view: {path =} {stub =} {page =}") @app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["POST"])
if not page: def edit(path):
return Response(f"{stub} ({path}) not found", status=404) uri = relative_uri()
return render_template( parent = get_parent(uri)
"page.html", app.web.logger.debug(f"Handling form submission: {uri = }, {parent = }, {request.form = }")
page=page, if not parent:
meta={ return Response(f"Parent for {uri} does not exist.", status=403)
'uri': path,
} 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 @app.web.after_request