remove forms, add validators
This commit is contained in:
parent
68cfd0e7f6
commit
fff34948d2
|
|
@ -165,12 +165,9 @@ API_URI=/_/v1/
|
||||||
else:
|
else:
|
||||||
search_uri = uri
|
search_uri = uri
|
||||||
|
|
||||||
# self.log.debug(f"Searching for page in {table = } with {search_uri = }; its parent is {parent_uri=}")
|
|
||||||
# self.log.debug("\n".join([f"{p.doc_id}: {p.uri}" for p in table.all()]))
|
|
||||||
page = table.get(where("uri") == search_uri, recurse=False)
|
page = table.get(where("uri") == search_uri, recurse=False)
|
||||||
if not page:
|
if not page:
|
||||||
# load the parent to check for write permissions
|
# load the parent to check for write permissions
|
||||||
# self.log.debug(f"Page at {search_uri} does not exist, looking for parent at {parent_uri=}")
|
|
||||||
parent_table = table if parent_uri and "/" in parent_uri else self.db.Page
|
parent_table = table if parent_uri and "/" in parent_uri else self.db.Page
|
||||||
parent = None
|
parent = None
|
||||||
try:
|
try:
|
||||||
|
|
@ -187,6 +184,9 @@ API_URI=/_/v1/
|
||||||
obj = getattr(schema, table.name)
|
obj = getattr(schema, table.name)
|
||||||
page = obj(name=page_name, body=obj.default.format(name=page_name), parent=parent)
|
page = obj(name=page_name, body=obj.default.format(name=page_name), parent=parent)
|
||||||
|
|
||||||
|
# validate the page name before we try to create anything
|
||||||
|
page._metadata.fields["name"].validate(page, db=self.db)
|
||||||
|
|
||||||
# self.log.debug(f"Returning {page.doc_id}: {page.uri}")
|
# self.log.debug(f"Returning {page.doc_id}: {page.uri}")
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,26 +43,26 @@ def bootstrap():
|
||||||
app.db.save(schema.User(name="__system__"))
|
app.db.save(schema.User(name="__system__"))
|
||||||
|
|
||||||
# create the top-level pages
|
# create the top-level pages
|
||||||
root = app.db.save(schema.Page(name=app.config.VIEW_URI, body=b"This is the home page", uri=""))
|
root = app.db.save(schema.Page(name=app.config.VIEW_URI, body="This is the home page", uri=""))
|
||||||
|
|
||||||
users = root.add_member(schema.Page(name="User", body=b"# Users\nusers go here."))
|
users = root.add_member(schema.Page(name="User", body="# Users\nusers go here."))
|
||||||
groups = root.add_member(schema.Page(name="Group", body=b"# Groups\ngroups go here."))
|
groups = root.add_member(schema.Page(name="Group", body="# Groups\ngroups go here."))
|
||||||
npcs = root.add_member(schema.Page(name="NPC", body=b"# NPCS!"))
|
npcs = root.add_member(schema.Page(name="NPC", body="# NPCS!"))
|
||||||
wiki = root.add_member(schema.Page(name="Wiki", body=TEMPLATE.encode()))
|
wiki = root.add_member(schema.Page(name="Wiki", body=TEMPLATE))
|
||||||
|
|
||||||
# create the NPCs
|
# create the NPCs
|
||||||
npcs.add_member(schema.NPC(name="Sabetha", body=""))
|
npcs.add_member(schema.NPC(name="Sabetha", body=""))
|
||||||
npcs.add_member(schema.NPC(name="John", body=""))
|
npcs.add_member(schema.NPC(name="John", body=""))
|
||||||
|
|
||||||
# create the users
|
# create the users
|
||||||
guest = users.add_member(schema.User(name="guest", body=b"# guest"))
|
guest = users.add_member(schema.User(name="guest", body="# guest"))
|
||||||
|
|
||||||
admin = users.add_member(
|
admin = users.add_member(
|
||||||
schema.User(name=app.config.ADMIN_USERNAME, password="fnord", email=app.config.ADMIN_EMAIL, body=b"# fnord")
|
schema.User(name=app.config.ADMIN_USERNAME, password="fnord", email=app.config.ADMIN_EMAIL, body="# fnord")
|
||||||
)
|
)
|
||||||
|
|
||||||
# create the admin user and admins group
|
# create the admin user and admins group
|
||||||
admins = groups.add_member(schema.Group(name="administrators", members=[admin], body=b"# administrators"))
|
admins = groups.add_member(schema.Group(name="administrators", members=[admin], body="# administrators"))
|
||||||
|
|
||||||
# admins get full access
|
# admins get full access
|
||||||
root.set_permissions(
|
root.set_permissions(
|
||||||
|
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from functools import cached_property
|
|
||||||
|
|
||||||
from flask import g
|
|
||||||
from grung.types import BackReference, Collection, Pointer, Record, Timestamp
|
|
||||||
|
|
||||||
from ttfrog import schema
|
|
||||||
|
|
||||||
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference, Timestamp]
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@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", "acl"]
|
|
||||||
|
|
||||||
def prepare(self):
|
|
||||||
for key, value in self.data.items():
|
|
||||||
if key in self.read_only:
|
|
||||||
continue
|
|
||||||
if self.record[key] != value:
|
|
||||||
self.record[key] = value
|
|
||||||
self.record.author = None if self.record == g.user else g.user
|
|
||||||
return self.record
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Page(Form):
|
|
||||||
"""
|
|
||||||
A form for creating and updating Page records.
|
|
||||||
"""
|
|
||||||
|
|
||||||
record: schema.Page
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Wiki(Form):
|
|
||||||
"""
|
|
||||||
A form for creating and updating Wiki records.
|
|
||||||
"""
|
|
||||||
|
|
||||||
record: schema.Page
|
|
||||||
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
from functools import cached_property
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from grung.types import (
|
from flask import g
|
||||||
|
from grung.objects import (
|
||||||
BackReference,
|
BackReference,
|
||||||
Collection,
|
Collection,
|
||||||
DateTime,
|
DateTime,
|
||||||
|
|
@ -17,8 +20,11 @@ from grung.types import (
|
||||||
TextFilePointer,
|
TextFilePointer,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
)
|
)
|
||||||
|
from grung.validators import PatternValidator
|
||||||
from tinydb import where
|
from tinydb import where
|
||||||
|
|
||||||
|
from ttfrog.exceptions import MalformedRequestError
|
||||||
|
|
||||||
|
|
||||||
def app_context():
|
def app_context():
|
||||||
import ttfrog.app
|
import ttfrog.app
|
||||||
|
|
@ -27,6 +33,9 @@ def app_context():
|
||||||
return ttfrog.app
|
return ttfrog.app
|
||||||
|
|
||||||
|
|
||||||
|
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference, Timestamp]
|
||||||
|
|
||||||
|
|
||||||
class Permissions(StrEnum):
|
class Permissions(StrEnum):
|
||||||
READ = "r"
|
READ = "r"
|
||||||
WRITE = "w"
|
WRITE = "w"
|
||||||
|
|
@ -57,17 +66,59 @@ class Page(Record):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
return [
|
return [
|
||||||
*super().fields(),
|
*super().fields(),
|
||||||
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 /
|
# The URI for the page, relative to the app's VIEW_URI
|
||||||
Collection("members", Page), # The pages that exist below this page's URI
|
Field("uri", unique=True, validators=[
|
||||||
Pointer("author", value_type=User), # The last user to touch the page.
|
PatternValidator(re.compile(
|
||||||
DateTime("created"), # When the page was created
|
r"""
|
||||||
Timestamp("last_modified"), # The last time the page was modified.
|
^ # match from beginning of line
|
||||||
|
(?:
|
||||||
|
/ # first char can be a /
|
||||||
|
| # or
|
||||||
|
(?:
|
||||||
|
[0-9a-z] # first char is alphanum
|
||||||
|
(?:/?[0-9a-z]+)* # following by more alphnum, maybe with a leading /
|
||||||
|
)+ # at least once, possibly many times,
|
||||||
|
)? # or URI could be entirely blank
|
||||||
|
$ # until the end of the string
|
||||||
|
""",
|
||||||
|
re.IGNORECASE | re.VERBOSE
|
||||||
|
))
|
||||||
|
]),
|
||||||
|
|
||||||
|
# The portion of the URI after the last /
|
||||||
|
Field("name", validators=[PatternValidator(re.compile(r'^(?:/|(?:[0-9a-z]+))$', re.IGNORECASE))]),
|
||||||
|
|
||||||
|
# The pages that exist below this page's URI
|
||||||
|
Collection("members", member_type=Page),
|
||||||
|
|
||||||
|
# The last user to touch the page.
|
||||||
|
Pointer("author", value_type=User),
|
||||||
|
|
||||||
|
# When the page was created
|
||||||
|
DateTime("created"),
|
||||||
|
|
||||||
|
# The last time the page was modified.
|
||||||
|
Timestamp("last_modified"),
|
||||||
|
|
||||||
|
# The main content blob of the page
|
||||||
|
TextFilePointer("body", extension='.md', default=Page.default),
|
||||||
|
|
||||||
|
# The access control list for this Page.
|
||||||
Dict("acl"),
|
Dict("acl"),
|
||||||
TextFilePointer("body", extension='.md', default=Page.default), # The main content blob of the page
|
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def read_only(self) -> set:
|
||||||
|
return [name for (name, attr) in self._metadata.fields.items() if type(attr) in READ_ONLY_FIELD_TYPES] + [
|
||||||
|
"uid",
|
||||||
|
"acl",
|
||||||
|
]
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
super().update(**dict((key, value) for key, value in kwargs.items() if key not in self.read_only))
|
||||||
|
|
||||||
def parent(self):
|
def parent(self):
|
||||||
if self.uri == "":
|
if self.uri == "":
|
||||||
return None
|
return None
|
||||||
|
|
@ -75,7 +126,7 @@ class Page(Record):
|
||||||
app = app_context()
|
app = app_context()
|
||||||
|
|
||||||
parent_uri = ""
|
parent_uri = ""
|
||||||
if "/" in self.uri:
|
if self.uri and "/" in self.uri:
|
||||||
parent_uri = self.uri.rsplit("/", 1)[0]
|
parent_uri = self.uri.rsplit("/", 1)[0]
|
||||||
for table_name in app.db.tables():
|
for table_name in app.db.tables():
|
||||||
page = app.db.table(table_name).get(where("uri") == parent_uri, recurse=False)
|
page = app.db.table(table_name).get(where("uri") == parent_uri, recurse=False)
|
||||||
|
|
@ -95,7 +146,7 @@ class Page(Record):
|
||||||
super().before_insert(app.db)
|
super().before_insert(app.db)
|
||||||
|
|
||||||
if not self.author:
|
if not self.author:
|
||||||
self.author = app.db.User.get(where('name') == '__system__')
|
self.author = app.db.User.get(where("name") == "__system__")
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
if not self.doc_id and self.created < now:
|
if not self.doc_id and self.created < now:
|
||||||
|
|
@ -192,6 +243,15 @@ class User(Entity):
|
||||||
Password("password"),
|
Password("password"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def update(self, **data):
|
||||||
|
self.author = None if self == g.user else g.user
|
||||||
|
return super().update(**data)
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
if self.name == "__system__":
|
||||||
|
raise MalformedRequestError("Invalid name.")
|
||||||
|
return super().validate()
|
||||||
|
|
||||||
def check_credentials(self, username: str, password: str) -> bool:
|
def check_credentials(self, username: str, password: str) -> bool:
|
||||||
return username == self.name and self._metadata.fields["password"].compare(password, self.password)
|
return username == self.name and self._metadata.fields["password"].compare(password, self.password)
|
||||||
|
|
||||||
|
|
@ -218,7 +278,7 @@ class Group(Entity):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fields(cls):
|
def fields(cls):
|
||||||
return super().fields() + [Collection("members", Entity)]
|
return super().fields() + [Collection("members", member_type=Entity)]
|
||||||
|
|
||||||
|
|
||||||
class NPC(Page):
|
class NPC(Page):
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from flask import Response, g, jsonify, redirect, render_template, request, session, url_for
|
from flask import Response, g, jsonify, redirect, render_template, request, session, url_for
|
||||||
|
from grung.exceptions import ValidationError
|
||||||
from tinydb import where
|
from tinydb import where
|
||||||
|
|
||||||
from ttfrog import app, forms, schema
|
from ttfrog import app, schema
|
||||||
from ttfrog.exceptions import MalformedRequestError, RecordNotFoundError, UnauthorizedError
|
from ttfrog.exceptions import MalformedRequestError, RecordNotFoundError, UnauthorizedError
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -21,6 +23,8 @@ def get_page(
|
||||||
except RecordNotFoundError as e:
|
except RecordNotFoundError as e:
|
||||||
if not create_okay:
|
if not create_okay:
|
||||||
return None, e
|
return None, e
|
||||||
|
except ValidationError as e:
|
||||||
|
return None, e
|
||||||
return page, None
|
return page, None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -63,8 +67,12 @@ def breadcrumbs():
|
||||||
def before_request():
|
def before_request():
|
||||||
g.messages = []
|
g.messages = []
|
||||||
if not request.path.startswith("/static"):
|
if not request.path.startswith("/static"):
|
||||||
user_id = session.get("user_id", 1)
|
user_id = session.get("user_id")
|
||||||
g.user = app.db.User.get(doc_id=user_id, recurse=False)
|
g.user = (
|
||||||
|
app.db.User.get(doc_id=user_id, recurse=False)
|
||||||
|
if user_id
|
||||||
|
else app.db.User.get(where("name") == "guest", recurse=False)
|
||||||
|
)
|
||||||
session["user_id"] = user_id
|
session["user_id"] = user_id
|
||||||
session["user"] = dict(g.user.serialize())
|
session["user"] = dict(g.user.serialize())
|
||||||
|
|
||||||
|
|
@ -109,9 +117,14 @@ def logout():
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
@app.web.route(f"{app.config.VIEW_URI}/<path:table>/<path:path>", methods=["GET"])
|
@app.web.route(f"{app.config.VIEW_URI}/<string:table>/<string:path>", methods=["GET"])
|
||||||
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["GET"], defaults={"table": "Page"})
|
@app.web.route(f"{app.config.VIEW_URI}/<string:path>", methods=["GET"], defaults={"table": "Page"})
|
||||||
def view(table, path):
|
def view(table, path):
|
||||||
|
clean_table = re.sub(r"[^a-zA-Z0-9]", "", unquote(table))
|
||||||
|
clean_path = re.sub(r"[^a-zA-Z0-9]", "", unquote(path))
|
||||||
|
if clean_table != table or clean_path != clean_path:
|
||||||
|
return redirect(url_for("view", table=clean_table, path=clean_path), 302)
|
||||||
|
|
||||||
page, error = get_page(request.path, table=table, create_okay=True)
|
page, error = get_page(request.path, table=table, create_okay=True)
|
||||||
if error:
|
if error:
|
||||||
g.messages.append(str(error))
|
g.messages.append(str(error))
|
||||||
|
|
@ -121,24 +134,23 @@ def view(table, path):
|
||||||
@app.web.route(f"{app.config.API_URI}/put/<path:table>/<path:path>", methods=["POST"])
|
@app.web.route(f"{app.config.API_URI}/put/<path:table>/<path:path>", methods=["POST"])
|
||||||
@app.web.route(f"{app.config.API_URI}/put/<path:path>", methods=["POST"], defaults={"table": "Page"})
|
@app.web.route(f"{app.config.API_URI}/put/<path:path>", methods=["POST"], defaults={"table": "Page"})
|
||||||
def put(table, path):
|
def put(table, path):
|
||||||
app.log.debug(f"Checking for page at {table}/{path} in {table} space")
|
clean_table = re.sub(r"[^a-zA-Z0-9]", "", unquote(table))
|
||||||
|
clean_path = re.sub(r"[^a-zA-Z0-9]", "", unquote(path))
|
||||||
|
if clean_table != table or clean_path != clean_path:
|
||||||
|
return redirect(url_for("put", table=clean_table, path=clean_path), 302)
|
||||||
|
|
||||||
page, error = get_page("/".join([table, path]), table=table, create_okay=True)
|
page, error = get_page("/".join([table, path]), table=table, create_okay=True)
|
||||||
app.log.debug(f"Found {page.doc_id}")
|
|
||||||
if error:
|
if error:
|
||||||
return api_response(error=error)
|
return api_response(error=error)
|
||||||
|
|
||||||
params = json.loads(request.data.decode())["body"]
|
page.update(**json.loads(request.data.decode())["body"])
|
||||||
save_data = getattr(forms, table)(page, params).prepare()
|
updated = app.db.save(page)
|
||||||
app.log.debug("Saving form data...")
|
|
||||||
app.log.debug(f"{save_data=}")
|
|
||||||
doc = app.db.save(save_data)
|
|
||||||
app.log.debug(f"Saved {dict(doc)}")
|
|
||||||
if not page.doc_id:
|
if not page.doc_id:
|
||||||
print(f"Adding {doc.doc_id} to {page.parent.members}")
|
parent = page.parent()
|
||||||
page.parent.members = list(set(page.parent.members + [doc]))
|
if parent:
|
||||||
app.db.save(page.parent)
|
parent.update(members=list(set(parent.members + [updated])))
|
||||||
app.log.debug(f"Saved: {dict(doc)=}")
|
app.db.save(parent)
|
||||||
return api_response(response=dict(doc))
|
return api_response(response=dict(updated))
|
||||||
|
|
||||||
|
|
||||||
@app.web.route(f"{app.config.API_URI}/search/<string:space>", methods=["POST"])
|
@app.web.route(f"{app.config.API_URI}/search/<string:space>", methods=["POST"])
|
||||||
|
|
@ -163,6 +175,10 @@ def search(space):
|
||||||
|
|
||||||
@app.web.route(f"{app.config.API_URI}/get/<path:table>/<int:doc_id>", methods=["GET"])
|
@app.web.route(f"{app.config.API_URI}/get/<path:table>/<int:doc_id>", methods=["GET"])
|
||||||
def get(table, doc_id):
|
def get(table, doc_id):
|
||||||
|
clean_table = re.sub(r"[^a-zA-Z0-9]", "", unquote(table))
|
||||||
|
if clean_table != table:
|
||||||
|
return redirect(url_for("get", table=clean_table, doc_id=doc_id), 302)
|
||||||
|
|
||||||
app.log.debug(f"API: getting {table}({doc_id})")
|
app.log.debug(f"API: getting {table}({doc_id})")
|
||||||
page, error = get_page(g.user, table=table, doc_id=doc_id)
|
page, error = get_page(g.user, table=table, doc_id=doc_id)
|
||||||
return api_response(response=dict(page), error=error)
|
return api_response(response=dict(page), error=error)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user