Compare commits
3 Commits
6afab2a15c
...
68cfd0e7f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68cfd0e7f6 | ||
|
|
f6efbeb54a | ||
|
|
5fffbf59f5 |
|
|
@ -153,45 +153,41 @@ API_URI=/_/v1/
|
||||||
if uri.startswith(self.config.VIEW_URI):
|
if uri.startswith(self.config.VIEW_URI):
|
||||||
uri = uri.replace(self.config.VIEW_URI, "", 1)
|
uri = uri.replace(self.config.VIEW_URI, "", 1)
|
||||||
|
|
||||||
parent_uri = ''
|
parent_uri = None
|
||||||
search_uri = '/'
|
search_uri = uri
|
||||||
page_name = '/'
|
page_name = uri
|
||||||
|
|
||||||
if "/" in uri:
|
if "/" in uri:
|
||||||
(parent_uri, page_name) = uri.rsplit("/", 1)
|
(parent_uri, page_name) = uri.rsplit("/", 1)
|
||||||
if parent_uri == 'Page':
|
if parent_uri == "Page":
|
||||||
parent_uri = '/'
|
parent_uri = None
|
||||||
search_uri = page_name
|
search_uri = page_name
|
||||||
else:
|
else:
|
||||||
search_uri = uri
|
search_uri = uri
|
||||||
elif uri:
|
|
||||||
parent_uri = "/"
|
|
||||||
search_uri = uri
|
|
||||||
page_name = uri
|
|
||||||
|
|
||||||
self.log.debug(f"Searching for page in {table = } with {search_uri = }; its parent is {parent_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()]))
|
# 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=}")
|
# self.log.debug(f"Page at {search_uri} does not exist, looking for parent at {parent_uri=}")
|
||||||
parent_table = table if "/" 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:
|
||||||
self.log.debug(f"Loading parent with {parent_uri}")
|
# self.log.debug(f"Loading parent with {parent_uri}")
|
||||||
parent = self.get_page(user, parent_table.name, uri=parent_uri)
|
parent = self.get_page(user, parent_table.name, uri=parent_uri)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug(f"Error loading parent: {e}")
|
self.log.debug(f"Error loading parent: {e}")
|
||||||
|
|
||||||
if not parent:
|
if not parent:
|
||||||
raise MalformedRequestError("Page does not exist and neither does its parent.")
|
raise MalformedRequestError(f"Page with uri '{search_uri}' does not exist and neither does its parent.")
|
||||||
if not self.authorize(user, parent, schema.Permissions.WRITE):
|
if not self.authorize(user, parent, schema.Permissions.WRITE):
|
||||||
raise UnauthorizedError(f"User {user.doc_id} does not have permission to create under {parent_uri}.")
|
raise UnauthorizedError(f"User {user.doc_id} does not have permission to create under {parent_uri}.")
|
||||||
page = getattr(schema, table.name)(
|
|
||||||
name=page_name, body=f"# {page_name}\nThis page does not exist", parent=parent
|
obj = getattr(schema, table.name)
|
||||||
)
|
page = obj(name=page_name, body=obj.default.format(name=page_name), parent=parent)
|
||||||
self.log.debug(f"Returning {page.doc_id}: {page.uri}")
|
|
||||||
|
# self.log.debug(f"Returning {page.doc_id}: {page.uri}")
|
||||||
return page
|
return page
|
||||||
|
|
||||||
def get_page(self, user, table_name, doc_id=None, uri=None):
|
def get_page(self, user, table_name, doc_id=None, uri=None):
|
||||||
|
|
@ -216,11 +212,8 @@ API_URI=/_/v1/
|
||||||
page = table.get(doc_id=doc_id)
|
page = table.get(doc_id=doc_id)
|
||||||
if not page:
|
if not page:
|
||||||
raise RecordNotFoundError(f"No record with {doc_id=} was found.")
|
raise RecordNotFoundError(f"No record with {doc_id=} was found.")
|
||||||
elif uri:
|
|
||||||
page = self._get_or_create_page_by_uri(user, table, uri)
|
page = self._get_or_create_page_by_uri(user, table, uri)
|
||||||
else:
|
|
||||||
self.log.error("No doc_id or uri.")
|
|
||||||
raise MalformedRequestError("Either a doc_id or a uri must be specified.")
|
|
||||||
|
|
||||||
if not self.authorize(user, page, schema.Permissions.READ):
|
if not self.authorize(user, page, schema.Permissions.READ):
|
||||||
self.log.error(f"No permission for {user.name} on {page}")
|
self.log.error(f"No permission for {user.name} on {page}")
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,11 @@ def bootstrap():
|
||||||
"""
|
"""
|
||||||
app.check_state()
|
app.check_state()
|
||||||
|
|
||||||
|
# the system user does not get added to the list of Users.
|
||||||
|
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"))
|
root = app.db.save(schema.Page(name=app.config.VIEW_URI, body=b"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=b"# 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=b"# Groups\ngroups go here."))
|
||||||
|
|
@ -53,6 +56,7 @@ def bootstrap():
|
||||||
|
|
||||||
# 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=b"# 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=b"# fnord")
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import logging
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
|
|
@ -9,6 +10,9 @@ from ttfrog import schema
|
||||||
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference, Timestamp]
|
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference, Timestamp]
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Form:
|
class Form:
|
||||||
"""
|
"""
|
||||||
|
|
@ -26,12 +30,11 @@ class Form:
|
||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
for key, value in self.data.items():
|
for key, value in self.data.items():
|
||||||
# filter out fields that cannot be set by the user
|
|
||||||
if key in self.read_only:
|
if key in self.read_only:
|
||||||
continue
|
continue
|
||||||
|
if self.record[key] != value:
|
||||||
self.record[key] = value
|
self.record[key] = value
|
||||||
self.record.author = g.user
|
self.record.author = None if self.record == g.user else g.user
|
||||||
return self.record
|
return self.record
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
from textwrap import dedent
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from grung.types import (
|
from grung.types import (
|
||||||
|
|
@ -19,6 +20,13 @@ from grung.types import (
|
||||||
from tinydb import where
|
from tinydb import where
|
||||||
|
|
||||||
|
|
||||||
|
def app_context():
|
||||||
|
import ttfrog.app
|
||||||
|
|
||||||
|
ttfrog.app.check_state()
|
||||||
|
return ttfrog.app
|
||||||
|
|
||||||
|
|
||||||
class Permissions(StrEnum):
|
class Permissions(StrEnum):
|
||||||
READ = "r"
|
READ = "r"
|
||||||
WRITE = "w"
|
WRITE = "w"
|
||||||
|
|
@ -30,6 +38,20 @@ class Page(Record):
|
||||||
A page in the wiki. Just about everything in the databse is either a Page or a subclass of a Page.
|
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.*
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fields(cls):
|
def fields(cls):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
|
@ -37,18 +59,30 @@ class Page(Record):
|
||||||
*super().fields(),
|
*super().fields(),
|
||||||
Field("uri", unique=True), # The URI for the page, relative to the app's VIEW_URI
|
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("name"), # The portion of the URI after the last /
|
||||||
TextFilePointer("body", extension='.md'), # The main content blob of the page
|
|
||||||
Collection("members", Page), # The pages that exist below this page's URI
|
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.
|
Pointer("author", value_type=User), # The last user to touch the page.
|
||||||
DateTime("created"), # When the page was created
|
DateTime("created"), # When the page was created
|
||||||
Timestamp("last_modified"), # The last time the page was modified.
|
Timestamp("last_modified"), # The last time the page was modified.
|
||||||
Dict("acl"),
|
Dict("acl"),
|
||||||
|
TextFilePointer("body", extension='.md', default=Page.default), # The main content blob of the page
|
||||||
]
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
# fmt: on
|
def parent(self):
|
||||||
|
if self.uri == "":
|
||||||
|
return None
|
||||||
|
|
||||||
def before_insert(self, db):
|
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
|
||||||
|
|
||||||
|
def before_insert(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Make the following adjustments before saving this record:
|
Make the following adjustments before saving this record:
|
||||||
* Derive the URI from the hierarchy of the parent.
|
* Derive the URI from the hierarchy of the parent.
|
||||||
|
|
@ -56,31 +90,25 @@ class Page(Record):
|
||||||
if not self.name:
|
if not self.name:
|
||||||
raise Exception("Must provide a name")
|
raise Exception("Must provide a name")
|
||||||
|
|
||||||
super().before_insert(db)
|
app = app_context()
|
||||||
|
|
||||||
|
super().before_insert(app.db)
|
||||||
|
|
||||||
|
if not self.author:
|
||||||
|
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:
|
||||||
self.created = now
|
self.created = now
|
||||||
|
|
||||||
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)
|
|
||||||
if not hasattr(self, "members"):
|
|
||||||
return
|
|
||||||
for child in self.members:
|
|
||||||
obj = BackReference.dereference(child, db)
|
|
||||||
obj.uri = f"{self.uri}/{obj.name}"
|
|
||||||
child = db.save(obj)
|
|
||||||
|
|
||||||
def add_member(self, child: Record):
|
def add_member(self, child: Record):
|
||||||
from ttfrog import app
|
app = app_context()
|
||||||
|
|
||||||
app.check_state()
|
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
|
||||||
self.members = list(set(self.members + [app.db.save(child)]))
|
self.members = list(set(self.members + [app.db.save(child)]))
|
||||||
app.db.save(self)
|
app.db.save(self)
|
||||||
return self.get_child(child)
|
return self.get_child(child)
|
||||||
|
|
@ -92,9 +120,7 @@ class Page(Record):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set_permissions(self, entity: Entity, permissions: List) -> str:
|
def set_permissions(self, entity: Entity, permissions: List) -> str:
|
||||||
from ttfrog import app
|
app = app_context()
|
||||||
|
|
||||||
app.check_state()
|
|
||||||
|
|
||||||
perms = "".join(permissions)
|
perms = "".join(permissions)
|
||||||
self.acl[entity.reference] = perms
|
self.acl[entity.reference] = perms
|
||||||
|
|
@ -106,9 +132,7 @@ class Page(Record):
|
||||||
Search upward through the page hierarchy looking for one with an ACL that either
|
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.
|
has a grant for the entity we care about, or at least one group in which the entity is a member.
|
||||||
"""
|
"""
|
||||||
from ttfrog import app
|
app = app_context()
|
||||||
|
|
||||||
app.check_state()
|
|
||||||
|
|
||||||
def find_acl(obj):
|
def find_acl(obj):
|
||||||
if hasattr(obj, "acl"):
|
if hasattr(obj, "acl"):
|
||||||
|
|
@ -126,7 +150,7 @@ class Page(Record):
|
||||||
return group_grants
|
return group_grants
|
||||||
|
|
||||||
if hasattr(obj, "parent"):
|
if hasattr(obj, "parent"):
|
||||||
return find_acl(obj.parent)
|
return find_acl(obj.parent())
|
||||||
return {"": ""}
|
return {"": ""}
|
||||||
|
|
||||||
return find_acl(self)
|
return find_acl(self)
|
||||||
|
|
@ -171,6 +195,21 @@ class User(Entity):
|
||||||
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class Group(Entity):
|
class Group(Entity):
|
||||||
"""
|
"""
|
||||||
|
|
@ -186,3 +225,21 @@ class NPC(Page):
|
||||||
"""
|
"""
|
||||||
An NPC, editable as a wiki 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]**
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
{% if user.can_write(page) %}
|
{% if user.can_write(page) %}
|
||||||
<script src="{{ url_for('static', filename='editor/toastui-editor-all.min.js' ) }}"></script>
|
<script src="{{ url_for('static', filename='editor/toastui-editor-all.min.js' ) }}"></script>
|
||||||
<script src="{{ url_for('static', filename='editor/editor.js' ) }}"></script>
|
<script src="{{ url_for('static', filename='editor/editor.js' ) }}"></script>
|
||||||
<script>initialize();</script>
|
<script>initialize("{{ app.config.VIEW_URI }}");</script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<script src="{{ url_for('static', filename='viewer/toastui-editor-viewer.min.js' ) }}"></script>
|
<script src="{{ url_for('static', filename='viewer/toastui-editor-viewer.min.js' ) }}"></script>
|
||||||
<script src="{{ url_for('static', filename='viewer/viewer.js' ) }}"></script>
|
<script src="{{ url_for('static', filename='viewer/viewer.js' ) }}"></script>
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,26 @@ var contents = null;
|
||||||
var pageContent = null;
|
var pageContent = null;
|
||||||
var saveButton = null;
|
var saveButton = null;
|
||||||
var editorUI = null;
|
var editorUI = null;
|
||||||
|
var VIEW_URI = null;;
|
||||||
|
|
||||||
APIv1 = {
|
APIv1 = {
|
||||||
|
get: function(doc_id, callback) {
|
||||||
|
(async () => {
|
||||||
|
const raw = await fetch('/_/v1/get/' + doc_id, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const res = await raw.json();
|
||||||
|
if (res['code'] != 200) {
|
||||||
|
console.error("APIv1 error: ", res)
|
||||||
|
}
|
||||||
|
callback(res);
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
|
||||||
put: function(data, callback) {
|
put: function(data, callback) {
|
||||||
(async () => {
|
(async () => {
|
||||||
const raw = await fetch('/_/v1/put/' + window.location.pathname, {
|
const raw = await fetch('/_/v1/put/' + window.location.pathname, {
|
||||||
|
|
@ -26,6 +44,26 @@ APIv1 = {
|
||||||
callback(res);
|
callback(res);
|
||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
search: function(space, query, callback) {
|
||||||
|
(async () => {
|
||||||
|
const raw = await fetch('/_/v1/search/' + space, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
'body': query
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const res = await raw.json();
|
||||||
|
if (res['code'] != 200) {
|
||||||
|
console.error("APIv1 error: ", res)
|
||||||
|
}
|
||||||
|
callback(res);
|
||||||
|
})();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
isReadOnly = function() {
|
isReadOnly = function() {
|
||||||
|
|
@ -108,8 +146,54 @@ toggleButton = function() {
|
||||||
handleContentChange = function() {
|
handleContentChange = function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize = function() {
|
autoComplete = function(search_string, matches, callback) {
|
||||||
return new toastui.Editor({
|
id = `_ac_${search_string}`;
|
||||||
|
el = document.getElementById(id);
|
||||||
|
var addEl = false;
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement("ul");
|
||||||
|
el.id = id;
|
||||||
|
addEl = true;
|
||||||
|
}
|
||||||
|
el.className = 'autocomplete';
|
||||||
|
el.innerHTML = "";
|
||||||
|
el.addEventListener("keyup", function(e) {
|
||||||
|
// do navigation / selection
|
||||||
|
});
|
||||||
|
|
||||||
|
matches.forEach(match => {
|
||||||
|
var text = match.uri;
|
||||||
|
for (pos = 0; pos < match.uri.length - search_string.length; pos++) {
|
||||||
|
var substr = match.name.substring(pos, search_string.length);
|
||||||
|
if (substr.toLowerCase() == search_string.toLowerCase()) {
|
||||||
|
text = match.name.substring(0, pos) + "<strong>" + substr + "</strong>" + match.name.substr(pos + substr.length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var option = document.createElement("li");
|
||||||
|
option.innerHTML = text;
|
||||||
|
option.addEventListener("click", function(e) {
|
||||||
|
// do selection
|
||||||
|
});
|
||||||
|
el.appendChild(option);
|
||||||
|
});
|
||||||
|
if (addEl) {
|
||||||
|
var selection = window.getSelection();
|
||||||
|
if (selection.rangeCount > 0) {
|
||||||
|
var range = selection.getRangeAt(0);
|
||||||
|
range.insertNode(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
initialize = function(base_uri) {
|
||||||
|
|
||||||
|
const macro_rule = /@(\S{3,})/;
|
||||||
|
|
||||||
|
VIEW_URI = base_uri;
|
||||||
|
|
||||||
|
const ed = new toastui.Editor({
|
||||||
el: editor,
|
el: editor,
|
||||||
initialEditType: 'wysiwyg',
|
initialEditType: 'wysiwyg',
|
||||||
initialValue: "",
|
initialValue: "",
|
||||||
|
|
@ -129,6 +213,23 @@ initialize = function() {
|
||||||
{ el: toggleButton(), tooltip: 'Toggle Edit Mode' }
|
{ el: toggleButton(), tooltip: 'Toggle Edit Mode' }
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
widgetRules: [ ],
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
rule: macro_rule, toDOM(text) {
|
||||||
|
const matched = text.match(macro_rule);
|
||||||
|
const search_string = matched[1];
|
||||||
|
|
||||||
|
var replacement = "";
|
||||||
|
|
||||||
|
});
|
||||||
|
console.log(replacement);
|
||||||
|
return replacement;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
*/
|
||||||
events: {
|
events: {
|
||||||
'loadUI': function(e) {
|
'loadUI': function(e) {
|
||||||
editorUI = e;
|
editorUI = e;
|
||||||
|
|
@ -147,4 +248,48 @@ initialize = function() {
|
||||||
'change': handleContentChange,
|
'change': handleContentChange,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var searchPos = null;
|
||||||
|
ed.on('keyup', (editorType, ev) => {
|
||||||
|
const [start, end] = editorUI.getSelection();
|
||||||
|
console.log(start, end);
|
||||||
|
if (ev.key === '@') {
|
||||||
|
searchPos = start;
|
||||||
|
console.log(`Setting search position to ${searchPos}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (searchPos === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var range = window.getSelection().getRangeAt(0);
|
||||||
|
range.selectNodeContents(editor);
|
||||||
|
range.setStart(editor, 0);
|
||||||
|
range.setEnd(editor, end);
|
||||||
|
var search_string = range.toString();
|
||||||
|
console.log(search_string);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
APIv1.search("", search_string, (res) => {
|
||||||
|
if (res.code == 404) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const matches = res.response;
|
||||||
|
if (matches.length == 1) {
|
||||||
|
replacement = document.createElement('span');
|
||||||
|
replacement.innerHTML = `<a class="tooltip-preview" data-uri="${matches[0].uri}" href="${VIEW_URI}${matches[0].uri}">${matches[0].name}</a>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
autoComplete(search_string, matches, (selection) => {
|
||||||
|
console.log(`Selected ${selection}`);
|
||||||
|
document.remove(options.id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
|
||||||
|
return ed;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
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 tinydb import where
|
||||||
|
|
||||||
from ttfrog import app, forms, schema
|
from ttfrog import app, forms, schema
|
||||||
from ttfrog.exceptions import MalformedRequestError, RecordNotFoundError, UnauthorizedError
|
from ttfrog.exceptions import MalformedRequestError, RecordNotFoundError, UnauthorizedError
|
||||||
|
|
@ -42,7 +44,7 @@ def rendered(page: schema.Record, template: str = "page.html"):
|
||||||
if not page:
|
if not page:
|
||||||
return Response("Page not found", status=404)
|
return Response("Page not found", status=404)
|
||||||
|
|
||||||
root = page if page.uri == app.config.VIEW_URI else get_page(app.config.VIEW_URI)[0]
|
root = page if page.uri == "" else get_page("")[0]
|
||||||
return render_template(template, page=page, app=app, breadcrumbs=breadcrumbs(), root=root, user=g.user, g=g)
|
return render_template(template, page=page, app=app, breadcrumbs=breadcrumbs(), root=root, user=g.user, g=g)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -62,7 +64,7 @@ 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", 1)
|
||||||
g.user = app.db.User.get(doc_id=user_id)
|
g.user = app.db.User.get(doc_id=user_id, recurse=False)
|
||||||
session["user_id"] = user_id
|
session["user_id"] = user_id
|
||||||
session["user"] = dict(g.user.serialize())
|
session["user"] = dict(g.user.serialize())
|
||||||
|
|
||||||
|
|
@ -77,7 +79,7 @@ def add_header(r):
|
||||||
|
|
||||||
@app.web.route(app.config.VIEW_URI)
|
@app.web.route(app.config.VIEW_URI)
|
||||||
def index():
|
def index():
|
||||||
page, error = get_page(app.config.VIEW_URI)
|
page, error = get_page("")
|
||||||
if error:
|
if error:
|
||||||
g.messages.append(str(error))
|
g.messages.append(str(error))
|
||||||
return rendered(page)
|
return rendered(page)
|
||||||
|
|
@ -127,12 +129,38 @@ def put(table, path):
|
||||||
|
|
||||||
params = json.loads(request.data.decode())["body"]
|
params = json.loads(request.data.decode())["body"]
|
||||||
save_data = getattr(forms, table)(page, params).prepare()
|
save_data = getattr(forms, table)(page, params).prepare()
|
||||||
|
app.log.debug("Saving form data...")
|
||||||
doc = app.db.save(save_data) if page.doc_id else page.parent.add_member(save_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:
|
||||||
|
print(f"Adding {doc.doc_id} to {page.parent.members}")
|
||||||
|
page.parent.members = list(set(page.parent.members + [doc]))
|
||||||
|
app.db.save(page.parent)
|
||||||
app.log.debug(f"Saved: {dict(doc)=}")
|
app.log.debug(f"Saved: {dict(doc)=}")
|
||||||
return api_response(response=dict(doc))
|
return api_response(response=dict(doc))
|
||||||
|
|
||||||
|
|
||||||
|
@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}'")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@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):
|
||||||
app.log.debug(f"API: getting {table}({doc_id})")
|
app.log.debug(f"API: getting {table}({doc_id})")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user