This commit is contained in:
evilchili 2025-10-04 01:26:09 -07:00
parent 120449386a
commit c9927656ce
8 changed files with 222 additions and 95 deletions

View File

@ -40,7 +40,7 @@ IN_MEMORY_DB=
DATA_ROOT=~/.dnd/ttfrog/
ADMIN_USERNAME=admin
ADMIN_EMAIL=
ADMIN_EMAIL=admin@telisar
THEME=default
@ -105,8 +105,8 @@ VIEW_URI=/
if not self._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))
def add_member(self, parent: schema.Page, child: schema.Page):
parent.members.append(self.db.save(child))
parent = self.db.save(parent)
return parent.get_child(child)
@ -115,34 +115,24 @@ VIEW_URI=/
Bootstrap the database entries by populating the first Page, the Admin user and the Admins group.
"""
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:
home = self.db.save(home)
except UniqueConstraintError:
pass
# create the top-level pages
root = self.db.save(schema.Page(name=self.config.VIEW_URI, title="Home", body="This is the home page"))
users = self.add_member(root, schema.Page(name="User", title="Users", body="users go here."))
groups = self.add_member(root, schema.Page(name="Group", title="Groups", body="groups go here."))
npcs = self.add_member(root, schema.Page(name="NPC", title="NPCS!", body="NPCS!"))
try:
npcs = self.add_page(home, npcs)
except UniqueConstraintError:
pass
# create the NPCs
sabetha = self.db.save(schema.NPC(name="Sabetha", body=""))
johns = self.db.save(schema.NPC(name="John", body=""))
self.add_member(npcs, sabetha)
self.add_member(npcs, johns)
try:
sabetha = self.add_page(npcs, sabetha)
except UniqueConstraintError:
pass
try:
admin = self.db.save(schema.User(name=self.config.ADMIN_USERNAME, email=self.config.ADMIN_EMAIL))
except UniqueConstraintError:
pass
try:
self.db.save(schema.Group(name="admins", users=[admin]))
except UniqueConstraintError:
pass
# create the admin user and admins group
admin = self.add_member(users, schema.User(name=self.config.ADMIN_USERNAME, email=self.config.ADMIN_EMAIL))
admins = self.db.save(schema.Group(name="administrators"))
admins = self.add_member(groups, admins)
self.add_member(admins, admin)
sys.modules[__name__] = ApplicationContext()

69
src/ttfrog/forms.py Normal file
View File

@ -0,0 +1,69 @@
from dataclasses import dataclass, field
from functools import cached_property
from grung.types import BackReference, Collection, Pointer, Record
from ttfrog import schema
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference]
@dataclass
class Form:
"""
The base Form controller for the web UI.
"""
record: Record
data: field(default_factory=dict)
@cached_property
def read_only(self) -> set:
return [
name for (name, attr) in self.record._metadata.fields.items() if type(attr) in READ_ONLY_FIELD_TYPES
] + ["uid"]
def prepare(self):
for key, value in self.data.items():
# filter out fields that cannot be set by the user
if key in self.read_only:
continue
self.record[key] = value
return self.record
@dataclass
class Page(Form):
"""
A form for creating and updating Page records.
"""
record: schema.Page
@cached_property
def read_only(self) -> set:
return set(list(super().read_only) + ["stub"])
@dataclass
class NPC(Page):
"""
A form for creating and updating Page records.
"""
record: schema.NPC
@dataclass
class User(Page):
"""
A form for creating and updating Page records.
"""
record: schema.NPC
@dataclass
class Group(Page):
"""
A form for creating and updating Page records.
"""
record: schema.NPC

View File

@ -1,52 +1,86 @@
from grung.types import BackReference, Collection, Field, Record
class User(Record):
@classmethod
def fields(cls):
return [*super().fields(), Field("name"), Field("email", unique=True)]
class Group(Record):
@classmethod
def fields(cls):
return [*super().fields(), Field("name", unique=True), Collection("users", User)]
from grung.types import BackReference, Collection, Field, Record, Pointer
class Page(Record):
"""
A page in the wiki. Just about everything in the databse is either a Page or a subclass of a Page.
"""
@classmethod
def fields(cls):
return [
*super().fields(),
Field("uri", unique=True),
Field("stub"),
Field("title"),
Field("body"),
Collection("pages", Page),
BackReference("parent", value_type=Page),
*super().fields(), # Pick up the UID and whatever other non-optional fields exist
Field("uri", unique=True), # The URI for the page, relative to the app's VIEW_URI
Field("name"), # The portion of the URI after the last /
Field("title"), # The page title
Field("body"), # The main content blob of the page
Collection("members", Page), # The pages that exist below this page's URI
BackReference("parent", value_type=Page), # The page that exists above this page's URI
Pointer("author", value_type=User), # The last user to touch the page.
# DateTime("last_modified"), # The last time the page was modified.
]
def before_insert(self, db):
"""
Make the following adjustments before saving this record:
* Derive the name from the title, or the title from the name
* Derive the URI from the hierarchy of the parent.
"""
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.name and not self.title:
raise Exception("Must provide either a name or a title!")
if not self.name:
self.name = self.title.title().replace(" ", "")
if not self.title:
self.title = self.stub
self.title = self.name
self.uri = (self.parent.uri + "/" if self.parent and self.parent.uri != "/" else "") + self.stub
self.uri = (self.parent.uri + "/" if self.parent and self.parent.uri != "/" else "") + self.name
def after_insert(self, db):
"""
After saving this record, ensure that any page in the members collection is updated with the
correct URI. This ensures that if a page is moved from one collection to another, the URI is updated.
"""
super().after_insert(db)
for child in self.pages:
if not hasattr(self, 'members'):
return
for child in self.members:
obj = BackReference.dereference(child, db)
obj.uri = f"{self.uri}/{obj.stub}"
obj.uri = f"{self.uri}/{obj.name}"
child = db.save(obj)
def get_child(self, obj: Record):
for page in self.pages:
for page in self.members:
if page.uid == obj.uid:
return page
return None
class User(Page):
"""
A website user, editable as a wiki page.
"""
@classmethod
def fields(cls):
return [
field
for field in [
*super().fields(),
Field("email", unique=True)
] if field.name != "members"
]
class Group(Page):
"""
A set of users, editable as a wiki page.
"""
class NPC(Page):
"""
An NPC, editable as a wiki page.
"""

View File

@ -12,8 +12,8 @@
</head>
<body>
<nav>
{% for uri, stub in breadcrumbs %}
<span>&nbsp; / <a href="{{ app.config.VIEW_URI }}{{ uri }}">{{ stub }}</a></span>
{% for uri, name in breadcrumbs %}
<span>&nbsp; / <a href="{{ app.config.VIEW_URI }}{{ uri }}">{{ name }}</a></span>
{% endfor %}
</nav>
<nav>

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block menu %}
{% for child in page.pages %}
<li><a href="{{ app.config['VIEW_URI'] }}/{{ child.uri }}">{{ child.title }}</a></li>
{% endfor %}
{% endblock %}
{% block content %}
<h1>Create: {{ page.title }}</h1>
<h2>{{ page.uri }}</h2>
{{ page.body }}
<pre>
{{ page }}
</pre>
<pre>
{{ app.web.config }}
</pre>
{% endblock %}

View File

@ -1,8 +1,8 @@
{% extends "base.html" %}
{% block menu %}
{% for child in page.pages %}
<li><a href="{{ app.config.VIEW_URI }}{{ child.uri }}">{{ child.title }}</a></li>
{% for member in page.members %}
<li><a href="{{ app.config.VIEW_URI }}{{ member.uri }}">{{ member.name }}</a></li>
{% endfor %}
{% endblock %}
{% block content %}

View File

@ -1,8 +1,7 @@
from flask import Response, render_template, request
from tinydb import where
from ttfrog import app, schema
from ttfrog.forms import PageForm
from ttfrog import app, schema, forms
STATIC = ["static"]
@ -15,30 +14,43 @@ def relative_uri(path: str = ""):
return (path or request.path).replace(app.config.VIEW_URI, "", 1).strip("/") or "/"
def get_parent(uri: str):
def get_parent(table: str, 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)
return get_page(parent_uri, table=table if '/' in parent_uri else 'Page', create_okay=False)
def get_page(path: str = "", create_okay: bool = False):
def get_page(path: str = "", table: str = "Page", create_okay: bool = False):
"""
Get one page, including its subpages, but not recursively.
Get one page, including its members, 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 = }")
uri = path.strip("/") or relative_uri(request.path)
app.web.logger.debug(f"Need a page in {table} for {uri = }")
if table not in app.db.tables():
return None
matches = app.db.table(table).search(where("uri") == uri, recurse=False)
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]
return getattr(schema, table)(
name=uri.split("/")[-1],
body="This page does not exist",
parent=get_parent(table, uri)
)
page = matches[0]
if hasattr(page, 'members'):
subpages = []
for pointer in matches[0].members:
table, uid = pointer.split("::")
subpages += app.db.table(table).search(where("uid") == uid, recurse=False)
page.members = subpages
return page
def rendered(page: schema.Record, template: str = "page.html"):
@ -53,15 +65,15 @@ def get_static(path):
def breadcrumbs():
"""
Return (uri, stub) pairs for the parents leading from the VIEW_URI to the current request.
Return (uri, name) 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)
yield (app.config.VIEW_URI, root.name)
uri = ""
for stub in relative_uri().split("/"):
uri = "/".join([uri, stub]).lstrip("/")
yield (uri, stub)
for name in relative_uri().split("/"):
uri = "/".join([uri, name]).lstrip("/")
yield (uri, name)
@app.web.route("/")
@ -69,31 +81,32 @@ def index():
return rendered(get_page(create_okay=False))
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["GET"])
def view(path):
return rendered(get_page(request.path, create_okay=True))
@app.web.route(f"{app.config.VIEW_URI}/<path:table>/<path:path>", methods=["GET"])
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["GET"], defaults={'table': 'Page'})
def view(table, path):
return rendered(get_page(request.path, table=table, create_okay=True))
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["POST"])
def edit(path):
@app.web.route(f"{app.config.VIEW_URI}/<path:table>/<path:path>", methods=["POST"])
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["POST"], defaults={'table': 'Page'})
def edit(table, path):
uri = relative_uri()
parent = get_parent(uri)
app.web.logger.debug(f"Handling form submission: {uri = }, {parent = }, {request.form = }")
parent = get_parent(table, uri)
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 = }")
# get or create the docoument at this uri
page = get_page(uri, table=table, create_okay=True)
save_data = getattr(forms, table)(page, request.form).prepare()
# editing existing document
if page.doc_id:
if page.uid != request.form["uid"]:
return Response("Invalid UID.", status=403)
return rendered(app.db.save(save_data))
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)
# saving a new document
return rendered(app.add_member(parent, save_data))
@app.web.after_request