diff --git a/src/ttfrog/app.py b/src/ttfrog/app.py index 03437fc..bde176c 100644 --- a/src/ttfrog/app.py +++ b/src/ttfrog/app.py @@ -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() diff --git a/src/ttfrog/forms.py b/src/ttfrog/forms.py new file mode 100644 index 0000000..cfa0e72 --- /dev/null +++ b/src/ttfrog/forms.py @@ -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 diff --git a/src/ttfrog/schema.py b/src/ttfrog/schema.py index d6e3548..1c99a39 100644 --- a/src/ttfrog/schema.py +++ b/src/ttfrog/schema.py @@ -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. + """ diff --git a/src/ttfrog/themes/default/base.html b/src/ttfrog/themes/default/base.html index 6320e05..6da2643 100644 --- a/src/ttfrog/themes/default/base.html +++ b/src/ttfrog/themes/default/base.html @@ -12,8 +12,8 @@