vtt/src/ttfrog/schema.py

240 lines
6.8 KiB
Python
Raw Normal View History

2025-10-05 00:15:37 -07:00
from __future__ import annotations
2025-09-24 01:28:23 -07:00
2025-10-05 00:15:37 -07:00
from datetime import datetime
2025-10-08 00:46:09 -07:00
from enum import StrEnum
2025-10-18 17:26:21 -07:00
from typing import List
from textwrap import dedent
2025-10-05 00:15:37 -07:00
2025-10-18 17:26:21 -07:00
from grung.types import (
BackReference,
Collection,
DateTime,
Dict,
Field,
Password,
Pointer,
Record,
TextFilePointer,
Timestamp,
)
2025-10-07 01:18:36 -07:00
from tinydb import where
def app_context():
import ttfrog.app
ttfrog.app.check_state()
return ttfrog.app
2025-10-08 00:46:09 -07:00
class Permissions(StrEnum):
READ = "r"
WRITE = "w"
DELETE = "d"
class Page(Record):
2025-10-04 01:26:09 -07:00
"""
A page in the wiki. Just about everything in the databse is either a Page or a subclass of a Page.
"""
default = dedent("""
# {name}
*Overview of this page*
## Section 1
*Organize your text into logically separted sections.*
""")
2025-10-04 01:26:09 -07:00
2025-09-28 14:14:16 -07:00
@classmethod
def fields(cls):
2025-10-07 01:18:36 -07:00
# fmt: off
2025-09-28 14:14:16 -07:00
return [
2025-10-08 00:46:09 -07:00
*super().fields(),
2025-10-07 01:18:36 -07:00
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 /
Collection("members", Page), # The pages that exist below this page's URI
Pointer("author", value_type=User), # The last user to touch the page.
DateTime("created"), # When the page was created
Timestamp("last_modified"), # The last time the page was modified.
2025-10-07 23:30:23 -07:00
Dict("acl"),
TextFilePointer("body", extension='.md', default=Page.default), # The main content blob of the page
2025-09-28 14:14:16 -07:00
]
# fmt: on
def parent(self):
if self.uri == "":
return None
app = app_context()
parent_uri = ""
if "/" in self.uri:
parent_uri = self.uri.rsplit("/", 1)[0]
for table_name in app.db.tables():
page = app.db.table(table_name).get(where("uri") == parent_uri, recurse=False)
if page:
return page
2025-09-28 14:14:16 -07:00
def before_insert(self, *args, **kwargs):
2025-10-04 01:26:09 -07:00
"""
Make the following adjustments before saving this record:
* Derive the URI from the hierarchy of the parent.
"""
2025-10-18 17:26:21 -07:00
if not self.name:
raise Exception("Must provide a name")
app = app_context()
super().before_insert(app.db)
if not self.author:
self.author = app.db.User.get(where('name') == '__system__')
2025-10-05 00:15:37 -07:00
now = datetime.utcnow()
if not self.doc_id and self.created < now:
self.created = now
2025-10-07 01:18:36 -07:00
def add_member(self, child: Record):
app = app_context()
2025-10-07 01:18:36 -07:00
prefix = (self.uri + "/") if self.uri else ""
new_uri = f"{prefix}{child.name}"
if child.uri != new_uri:
app.log.debug(f"Moving {child._metadata.table}[{child.doc_id}] from {child.uri} to {new_uri}")
child.uri = new_uri
2025-10-07 01:18:36 -07:00
self.members = list(set(self.members + [app.db.save(child)]))
app.db.save(self)
return self.get_child(child)
def get_child(self, obj: Record):
2025-10-04 01:26:09 -07:00
for page in self.members:
2025-10-08 00:46:09 -07:00
if page[page._metadata.primary_key] == obj[obj._metadata.primary_key]:
return page
return None
2025-10-04 01:26:09 -07:00
2025-10-07 23:30:23 -07:00
def set_permissions(self, entity: Entity, permissions: List) -> str:
app = app_context()
2025-10-07 01:18:36 -07:00
2025-10-07 23:30:23 -07:00
perms = "".join(permissions)
self.acl[entity.reference] = perms
2025-10-07 01:18:36 -07:00
app.db.save(self)
2025-10-05 00:15:37 -07:00
return perms
2025-10-07 23:30:23 -07:00
def get_acl_for_entity(self, entity) -> (str, str | None):
2025-10-07 01:18:36 -07:00
"""
Search upward through the page hierarchy looking for one with an ACL that either
has a grant for the entity we care about, or at least one group in which the entity is a member.
"""
app = app_context()
2025-10-06 17:50:10 -07:00
def find_acl(obj):
2025-10-07 01:18:36 -07:00
if hasattr(obj, "acl"):
2025-10-07 23:30:23 -07:00
if entity.reference in obj.acl:
return {entity.reference: obj.acl[entity.reference]}
group_grants = {}
for ref, grant in obj.acl.items():
2025-10-08 00:46:09 -07:00
(table_name, pkey, pval) = ref.split("::")
if table_name == "Group":
group = app.db.Group.get(where(pkey) == pval, recurse=False)
2025-10-07 23:30:23 -07:00
if entity.reference in group.members:
group_grants[ref] = grant
2025-10-07 01:18:36 -07:00
if group_grants:
return group_grants
2025-10-07 23:30:23 -07:00
if hasattr(obj, "parent"):
return find_acl(obj.parent())
2025-10-07 23:30:23 -07:00
return {"": ""}
2025-10-06 17:50:10 -07:00
2025-10-07 01:18:36 -07:00
return find_acl(self)
2025-10-05 00:15:37 -07:00
2025-10-07 01:18:36 -07:00
class Entity(Page):
2025-10-08 00:46:09 -07:00
@classmethod
def fields(cls):
2025-10-18 17:26:21 -07:00
inherited = [field for field in super().fields() if field.name not in ("members", "uid")]
2025-10-08 00:46:09 -07:00
return inherited + [
Field("name", primary_key=True),
]
2025-10-07 01:18:36 -07:00
def has_permission(self, record: Record, requested: str) -> bool | None:
2025-10-07 23:30:23 -07:00
for entity, grants in record.get_acl_for_entity(self).items():
if requested in grants:
2025-10-05 00:15:37 -07:00
return True
return False
2025-10-07 01:18:36 -07:00
def can_read(self, record: Record):
return self.has_permission(record, Permissions.READ)
2025-10-05 00:15:37 -07:00
2025-10-07 01:18:36 -07:00
def can_write(self, record: Record):
return self.has_permission(record, Permissions.WRITE)
2025-10-04 01:26:09 -07:00
2025-10-07 01:18:36 -07:00
def can_delete(self, record: Record):
return self.has_permission(record, Permissions.DELETE)
2025-10-05 00:15:37 -07:00
class User(Entity):
2025-10-04 01:26:09 -07:00
"""
A website user, editable as a wiki page.
"""
2025-10-07 01:18:36 -07:00
2025-10-04 01:26:09 -07:00
@classmethod
def fields(cls):
2025-10-08 00:46:09 -07:00
return super().fields() + [
Field("email", unique=True),
Password("password"),
2025-10-04 01:26:09 -07:00
]
2025-10-08 00:46:09 -07:00
def check_credentials(self, username: str, password: str) -> bool:
return username == self.name and self._metadata.fields["password"].compare(password, self.password)
def after_insert(self, *args, **kwargs):
"""
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.
"""
app = app_context()
super().after_insert(app.db)
if not hasattr(self, "members"):
return
for child in self.members:
obj = BackReference.dereference(child, app.db, recurse=False)
obj.uri = f"{self.uri}/{obj.name}"
child = app.db.save(obj)
2025-10-04 01:26:09 -07:00
2025-10-05 00:15:37 -07:00
class Group(Entity):
2025-10-04 01:26:09 -07:00
"""
A set of users, editable as a wiki page.
"""
2025-10-18 17:26:21 -07:00
2025-10-08 00:46:09 -07:00
@classmethod
def fields(cls):
2025-10-18 17:26:21 -07:00
return super().fields() + [Collection("members", Entity)]
2025-10-04 01:26:09 -07:00
class NPC(Page):
"""
An NPC, editable as a wiki page.
"""
default = dedent("""
# {name}
*[Ancestry] [Class]*
| AC | HP | STR | DEX | CON | INT | WIS | CHA
|----|----|-----|-----|-----|-----|-----|------
| 10 | 10 | +0 | +0 | +0 | +0 | +0 | +0
**{name} (they/they)** [description]
* Personality: **[keywords]**
* Flaw: **[flaw]**
* Goal: **[goal]**
""")