2025-10-22 19:20:47 -07:00
|
|
|
import json
|
2025-10-03 16:44:25 -07:00
|
|
|
|
2025-10-22 19:20:47 -07:00
|
|
|
from flask import Response, g, jsonify, redirect, render_template, request, session, url_for
|
2025-10-03 16:44:25 -07:00
|
|
|
|
2025-10-22 19:20:47 -07:00
|
|
|
from ttfrog import app, forms, schema
|
|
|
|
|
from ttfrog.exceptions import MalformedRequestError, RecordNotFoundError, UnauthorizedError
|
2025-10-29 19:06:57 -07:00
|
|
|
from tinydb import where
|
|
|
|
|
import re
|
2025-10-03 16:44:25 -07:00
|
|
|
|
2025-10-04 01:26:09 -07:00
|
|
|
|
2025-10-22 19:20:47 -07:00
|
|
|
def get_page(
|
|
|
|
|
path: str, table: str = "Page", doc_id: int = None, create_okay: bool = False
|
|
|
|
|
) -> (schema.Record | None, Exception | None):
|
2025-09-28 14:14:16 -07:00
|
|
|
"""
|
2025-10-04 01:26:09 -07:00
|
|
|
Get one page, including its members, but not recursively.
|
2025-09-28 14:14:16 -07:00
|
|
|
"""
|
2025-10-22 19:20:47 -07:00
|
|
|
try:
|
|
|
|
|
page = app.get_page(g.user, table, doc_id=doc_id, uri=path)
|
|
|
|
|
except (UnauthorizedError, MalformedRequestError) as e:
|
|
|
|
|
return None, e
|
|
|
|
|
except RecordNotFoundError as e:
|
2025-10-03 16:44:25 -07:00
|
|
|
if not create_okay:
|
2025-10-22 19:20:47 -07:00
|
|
|
return None, e
|
|
|
|
|
return page, None
|
2025-10-05 00:15:37 -07:00
|
|
|
|
|
|
|
|
|
2025-10-22 19:20:47 -07:00
|
|
|
def api_response(response={}, messages=[], error=None):
|
|
|
|
|
response_code = 200
|
2025-10-04 01:26:09 -07:00
|
|
|
|
2025-10-22 19:20:47 -07:00
|
|
|
if error:
|
|
|
|
|
response_code = 500
|
|
|
|
|
response = {}
|
|
|
|
|
if isinstance(error, UnauthorizedError):
|
|
|
|
|
response_code = 403
|
|
|
|
|
elif isinstance(error, MalformedRequestError):
|
|
|
|
|
response_code = 4000
|
|
|
|
|
elif isinstance(error, RecordNotFoundError):
|
|
|
|
|
response_code = 404
|
|
|
|
|
|
|
|
|
|
return jsonify({"messages": messages, "response": response, "code": response_code}), response_code
|
2025-09-21 22:11:56 -07:00
|
|
|
|
2025-09-27 16:20:08 -07:00
|
|
|
|
2025-10-03 16:44:25 -07:00
|
|
|
def rendered(page: schema.Record, template: str = "page.html"):
|
|
|
|
|
if not page:
|
|
|
|
|
return Response("Page not found", status=404)
|
|
|
|
|
|
2025-10-29 19:06:57 -07:00
|
|
|
root = page if page.uri == "" else get_page("")[0]
|
2025-10-22 19:20:47 -07:00
|
|
|
return render_template(template, page=page, app=app, breadcrumbs=breadcrumbs(), root=root, user=g.user, g=g)
|
2025-10-03 16:44:25 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def breadcrumbs():
|
|
|
|
|
"""
|
2025-10-04 01:26:09 -07:00
|
|
|
Return (uri, name) pairs for the parents leading from the VIEW_URI to the current request.
|
2025-10-03 16:44:25 -07:00
|
|
|
"""
|
|
|
|
|
uri = ""
|
2025-10-22 19:20:47 -07:00
|
|
|
names = (request.path.replace(app.config.VIEW_URI, "", 1).strip("/") or "/").split("/")
|
|
|
|
|
for name in names:
|
2025-10-12 15:36:38 -07:00
|
|
|
uri = "/".join([uri, name])
|
2025-10-04 01:26:09 -07:00
|
|
|
yield (uri, name)
|
2025-10-03 16:44:25 -07:00
|
|
|
|
|
|
|
|
|
2025-10-22 19:20:47 -07:00
|
|
|
@app.web.before_request
|
|
|
|
|
def before_request():
|
|
|
|
|
g.messages = []
|
|
|
|
|
if not request.path.startswith("/static"):
|
|
|
|
|
user_id = session.get("user_id", 1)
|
2025-10-29 19:06:57 -07:00
|
|
|
g.user = app.db.User.get(doc_id=user_id, recurse=False)
|
2025-10-22 19:20:47 -07:00
|
|
|
session["user_id"] = user_id
|
|
|
|
|
session["user"] = dict(g.user.serialize())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.web.after_request
|
|
|
|
|
def add_header(r):
|
|
|
|
|
r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, public, max-age=0"
|
|
|
|
|
r.headers["Pragma"] = "no-cache"
|
|
|
|
|
r.headers["Expires"] = "0"
|
|
|
|
|
return r
|
|
|
|
|
|
|
|
|
|
|
2025-10-12 15:36:38 -07:00
|
|
|
@app.web.route(app.config.VIEW_URI)
|
2025-09-27 16:20:08 -07:00
|
|
|
def index():
|
2025-10-29 19:06:57 -07:00
|
|
|
page, error = get_page("")
|
2025-10-22 19:20:47 -07:00
|
|
|
if error:
|
|
|
|
|
g.messages.append(str(error))
|
|
|
|
|
return rendered(page)
|
2025-09-27 16:20:08 -07:00
|
|
|
|
|
|
|
|
|
2025-10-05 00:15:37 -07:00
|
|
|
@app.web.route("/login", methods=["GET", "POST"])
|
2025-10-04 10:48:18 -07:00
|
|
|
def login():
|
|
|
|
|
app.web.session_interface.regenerate(session)
|
2025-10-05 00:15:37 -07:00
|
|
|
if request.method == "POST":
|
|
|
|
|
username = request.form.get("username")
|
|
|
|
|
password = request.form.get("password")
|
2025-10-07 01:18:36 -07:00
|
|
|
user = app.authenticate(username, password)
|
|
|
|
|
if user:
|
|
|
|
|
g.user = user
|
|
|
|
|
session["user_id"] = g.user.doc_id
|
|
|
|
|
session["user"] = dict(g.user.serialize())
|
2025-10-05 00:15:37 -07:00
|
|
|
return redirect(url_for("index"))
|
|
|
|
|
g.messages.append(f"Invalid login for {username}")
|
2025-10-22 19:20:47 -07:00
|
|
|
return rendered(schema.Page(name="Login"), "login.html")
|
2025-10-04 10:48:18 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.web.route("/logout")
|
|
|
|
|
def logout():
|
2025-10-05 00:15:37 -07:00
|
|
|
if "user_id" in session:
|
|
|
|
|
del session["user_id"]
|
2025-10-04 10:48:18 -07:00
|
|
|
del g.user
|
2025-10-08 00:46:09 -07:00
|
|
|
return redirect(url_for("index"))
|
2025-10-04 10:48:18 -07:00
|
|
|
|
|
|
|
|
|
2025-10-04 01:26:09 -07:00
|
|
|
@app.web.route(f"{app.config.VIEW_URI}/<path:table>/<path:path>", methods=["GET"])
|
2025-10-05 00:15:37 -07:00
|
|
|
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["GET"], defaults={"table": "Page"})
|
2025-10-04 01:26:09 -07:00
|
|
|
def view(table, path):
|
2025-10-22 19:20:47 -07:00
|
|
|
page, error = get_page(request.path, table=table, create_okay=True)
|
|
|
|
|
if error:
|
|
|
|
|
g.messages.append(str(error))
|
2025-10-12 15:36:38 -07:00
|
|
|
return rendered(page)
|
2025-10-03 16:44:25 -07:00
|
|
|
|
|
|
|
|
|
2025-10-22 19:20:47 -07:00
|
|
|
@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"})
|
|
|
|
|
def put(table, path):
|
|
|
|
|
app.log.debug(f"Checking for page at {table}/{path} in {table} space")
|
|
|
|
|
page, error = get_page("/".join([table, path]), table=table, create_okay=True)
|
|
|
|
|
app.log.debug(f"Found {page.doc_id}")
|
|
|
|
|
if error:
|
|
|
|
|
return api_response(error=error)
|
2025-10-04 01:26:09 -07:00
|
|
|
|
2025-10-22 19:20:47 -07:00
|
|
|
params = json.loads(request.data.decode())["body"]
|
|
|
|
|
save_data = getattr(forms, table)(page, params).prepare()
|
2025-10-29 19:06:57 -07:00
|
|
|
app.log.debug("Saving form data...")
|
|
|
|
|
doc = app.db.save(save_data)
|
|
|
|
|
app.log.debug(f"Saved {dict(doc)}")
|
|
|
|
|
if not page.doc_id:
|
|
|
|
|
print(f"Adding {doc.doc_id} to {page.parent.members}")
|
|
|
|
|
page.parent.members = list(set(page.parent.members + [doc]))
|
|
|
|
|
app.db.save(page.parent)
|
2025-10-22 19:20:47 -07:00
|
|
|
app.log.debug(f"Saved: {dict(doc)=}")
|
|
|
|
|
return api_response(response=dict(doc))
|
2025-10-04 10:48:18 -07:00
|
|
|
|
|
|
|
|
|
2025-10-29 19:06:57 -07:00
|
|
|
@app.web.route(f"{app.config.API_URI}/search/<string:space>", methods=["POST"])
|
|
|
|
|
@app.web.route(f"{app.config.API_URI}/search/", methods=["POST"], defaults={"space": None})
|
|
|
|
|
def search(space):
|
|
|
|
|
|
|
|
|
|
spaces = app.db.tables()
|
|
|
|
|
if space:
|
|
|
|
|
spaces = [space.lower().capitalize()]
|
|
|
|
|
|
|
|
|
|
query = json.loads(request.data.decode())["body"]
|
|
|
|
|
app.log.debug(f"Searching for records matching query {query}")
|
|
|
|
|
matches = []
|
|
|
|
|
for space in spaces:
|
|
|
|
|
for page in app.db.table(space).search(where('name').matches(query, re.IGNORECASE), recurse=False):
|
|
|
|
|
if app.authorize(g.user, page, schema.Permissions.READ):
|
|
|
|
|
app.log.debug(f"Adding search result {dict(page)}")
|
|
|
|
|
matches.append(dict(page))
|
|
|
|
|
return api_response(
|
|
|
|
|
response=matches,
|
|
|
|
|
error=None if matches else RecordNotFoundError(f"No records matching '{query}'")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2025-10-22 19:20:47 -07:00
|
|
|
@app.web.route(f"{app.config.API_URI}/get/<path:table>/<int:doc_id>", methods=["GET"])
|
|
|
|
|
def get(table, doc_id):
|
|
|
|
|
app.log.debug(f"API: getting {table}({doc_id})")
|
|
|
|
|
page, error = get_page(g.user, table=table, doc_id=doc_id)
|
|
|
|
|
return api_response(response=dict(page), error=error)
|