diff --git a/ttfrog/assets/templates/character_sheet.html b/ttfrog/assets/templates/character_sheet.html
index 1dd43d1..e4a0c11 100644
--- a/ttfrog/assets/templates/character_sheet.html
+++ b/ttfrog/assets/templates/character_sheet.html
@@ -1,5 +1,15 @@
+
+
{{ character.name }}
{{ form.display(value=character) }}
+{{ flash }}
+
{{ character }}
+
diff --git a/ttfrog/db/bootstrap.py b/ttfrog/db/bootstrap.py
index 37dec18..a8c0cb2 100644
--- a/ttfrog/db/bootstrap.py
+++ b/ttfrog/db/bootstrap.py
@@ -1,5 +1,3 @@
-import base64
-import hashlib
import logging
from ttfrog.db import db, session
@@ -18,17 +16,6 @@ data = {
}
-def slug_from_rec(rec):
- """
- Create a uniquish slug from a dictionary.
- """
- sha1bytes = hashlib.sha1(str(rec).encode())
- return '-'.join([
- base64.urlsafe_b64encode(sha1bytes.digest()).decode("ascii")[:10],
- rec.get('name', '') # will need to normalize this for URLs
- ])
-
-
def bootstrap():
"""
Initialize the database with source data. Idempotent; will skip anything that already exists.
@@ -39,13 +26,17 @@ def bootstrap():
logging.debug("No bootstrap data for table {table_name}; skipping.")
continue
for rec in data[table_name]:
- if 'slug' in table.columns:
- rec['slug'] = slug_from_rec(rec)
stmt = table.insert().values(**rec).prefix_with("OR IGNORE")
- result = session.execute(stmt)
- session.commit()
- last_id = result.inserted_primary_key[0]
- if last_id == 0:
+ result, error = db.execute(stmt)
+ if error:
+ raise RuntimeError(error)
+
+ rec['id'] = result.inserted_primary_key[0]
+ if rec['id'] == 0:
logging.info(f"Skipped existing {table_name} {rec}")
- else:
- logging.info(f"Created {table_name} {result.inserted_primary_key[0]}: {rec}")
+ continue
+
+ if 'slug' in table.columns:
+ rec['slug'] = db.slugify(rec)
+ db.update(table, **rec)
+ logging.info(f"Created {table_name} {rec}")
diff --git a/ttfrog/db/manager.py b/ttfrog/db/manager.py
index 12cdf22..54bab4e 100644
--- a/ttfrog/db/manager.py
+++ b/ttfrog/db/manager.py
@@ -1,8 +1,12 @@
-from functools import cached_property
+import base64
+import hashlib
import logging
+from functools import cached_property
+
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
+from sqlalchemy.exc import IntegrityError
from ttfrog.path import database
from ttfrog.db.schema import metadata
@@ -32,17 +36,39 @@ class SQLDatabaseManager:
def query(self, *args, **kwargs):
return self.DBSession.query(*args, **kwargs)
+ def execute(self, statement) -> tuple:
+ logging.debug(statement)
+ result = None
+ error = None
+ try:
+ result = self.DBSession.execute(statement)
+ self.DBSession.commit()
+ except IntegrityError as exc:
+ logging.error(exc)
+ error = "An error occurred when saving changes."
+ return result, error
+
+ def insert(self, table, **kwargs) -> tuple:
+ stmt = table.insert().values(**kwargs)
+ return self.execute(stmt)
+
def update(self, table, **kwargs):
- stmt = table.update().values(**kwargs)
- logging.debug(stmt)
- result = self.DBSession.execute(stmt)
- self.DBSession.commit()
- return result
+ primary_key = kwargs.pop('id')
+ stmt = table.update().values(**kwargs).where(table.columns.id == primary_key)
+ return self.execute(stmt)
def init_model(self, engine=None):
metadata.create_all(bind=engine or self.engine)
return self.DBSession
+ def slugify(self, rec: dict) -> str:
+ """
+ Create a uniquish slug from a dictionary.
+ """
+ sha1bytes = hashlib.sha1(str(rec['id']).encode())
+ return base64.urlsafe_b64encode(sha1bytes.digest()).decode("ascii")[:10]
+
+
def __getattr__(self, name: str):
try:
return self.tables[name]
diff --git a/ttfrog/db/schema.py b/ttfrog/db/schema.py
index 995e453..0eca673 100644
--- a/ttfrog/db/schema.py
+++ b/ttfrog/db/schema.py
@@ -14,7 +14,8 @@ metadata = MetaData()
Ancestry = Table(
"ancestry",
metadata,
- Column("name", String, primary_key=True),
+ Column("id", Integer, primary_key=True, autoincrement=True),
+ Column("name", String, index=True, unique=True),
Column("slug", String, index=True, unique=True),
Column("description", UnicodeText),
)
diff --git a/ttfrog/webserver/application.py b/ttfrog/webserver/application.py
index c681686..2d3a5c3 100644
--- a/ttfrog/webserver/application.py
+++ b/ttfrog/webserver/application.py
@@ -9,7 +9,7 @@ from wsgiref.simple_server import make_server
import webhelpers2
import tw2.core
-from ttfrog.webserver.controllers import RootController
+from ttfrog.webserver.controllers.root import RootController
from ttfrog.db import db
import ttfrog.path
diff --git a/ttfrog/webserver/controllers/__init__.py b/ttfrog/webserver/controllers/__init__.py
index c4f55f7..e69de29 100644
--- a/ttfrog/webserver/controllers/__init__.py
+++ b/ttfrog/webserver/controllers/__init__.py
@@ -1,3 +0,0 @@
-from .root import RootController
-
-__ALL__ = [RootController]
diff --git a/ttfrog/webserver/controllers/base.py b/ttfrog/webserver/controllers/base.py
new file mode 100644
index 0000000..88aa42c
--- /dev/null
+++ b/ttfrog/webserver/controllers/base.py
@@ -0,0 +1,19 @@
+import inspect
+
+from tg import flash
+from tg import TGController
+from tg import tmpl_context
+from markupsafe import Markup
+
+
+class BaseController(TGController):
+
+ def _before(self, *args, **kwargs):
+ tmpl_context.project_name = 'TableTop Frog'
+
+ def output(self, **kwargs) -> dict:
+ return dict(
+ page=inspect.stack()[1].function,
+ flash=Markup(flash.render('flash', use_js=False)),
+ **kwargs,
+ )
diff --git a/ttfrog/webserver/controllers/character_sheet.py b/ttfrog/webserver/controllers/character_sheet.py
index 08a0e84..8ece14b 100644
--- a/ttfrog/webserver/controllers/character_sheet.py
+++ b/ttfrog/webserver/controllers/character_sheet.py
@@ -1,29 +1,72 @@
+import base64
+import hashlib
+import logging
+
from tg import expose
-from tg import TGController
+from tg import flash
+from tg import validate
+from tg.controllers.util import redirect
from ttfrog.db import db
from ttfrog.db.schema import Character
+from ttfrog.webserver.controllers.base import BaseController
from ttfrog.webserver.widgets import CharacterSheet
-class CharacterSheetController(TGController):
+class CharacterSheetController(BaseController):
@expose()
def _lookup(self, *parts):
- return FormController(parts[0]), parts[1:]
+ slug = parts[0] if parts else ''
+ return FormController(slug), parts[1:] if len(parts) > 1 else []
-class FormController:
+class FormController(BaseController):
def __init__(self, slug: str):
+ super().__init__()
self.character = dict()
if slug:
- self.load(slug)
+ self.load_from_slug(slug)
- def load(self, slug: str) -> None:
+ @property
+ def uri(self):
+ if self.character:
+ return f"/sheet/{self.character['slug']}/{self.character['name']}"
+ else:
+ return None
+
+ @property
+ def all_characters(self):
+ return [row._mapping for row in db.query(Character).all()]
+
+ def load_from_slug(self, slug) -> None:
self.character = db.query(Character).filter(Character.columns.slug == slug)[0]._mapping
+ def save(self, fields) -> str:
+ rec = dict()
+ if not self.character:
+ result, error = db.insert(Character, **fields)
+ if error:
+ return error
+ fields['id'] = result.inserted_primary_key[0]
+ fields['slug'] = db.slugify(fields)
+ else:
+ rec = dict(**self.character)
+
+ rec.update(**fields)
+ result, error = db.update(Character, **rec)
+ self.load_from_slug(rec['slug'])
+ if not error:
+ flash(f"{self.character['name']} updated!")
+ return redirect(self.uri)
+ flash(error)
+
@expose('character_sheet.html')
- def _default(self, *args, **kwargs):
- if kwargs:
- db.update(Character, **kwargs)
- self.load(self.character['slug'])
- return dict(page='sheet', form=CharacterSheet, character=self.character)
+ @validate(form=CharacterSheet)
+ def _default(self, *args, **fields):
+ if fields:
+ return self.save(fields)
+ return self.output(
+ form=CharacterSheet,
+ character=self.character,
+ all_characters=self.all_characters,
+ )
diff --git a/ttfrog/webserver/controllers/root.py b/ttfrog/webserver/controllers/root.py
index bec9a01..6790ce5 100644
--- a/ttfrog/webserver/controllers/root.py
+++ b/ttfrog/webserver/controllers/root.py
@@ -1,18 +1,16 @@
from tg import expose
-from tg import TGController
-from tg import tmpl_context
+
from ttfrog.db import db
+from ttfrog.webserver.controllers.base import BaseController
from ttfrog.webserver.controllers.character_sheet import CharacterSheetController
-class RootController(TGController):
+class RootController(BaseController):
+ # serve character sheet interface on /sheet
sheet = CharacterSheetController()
- def _before(self, *args, **kwargs):
- tmpl_context.project_name = 'TableTop Frog'
-
@expose('index.html')
def index(self):
ancestries = [row._mapping for row in db.query(db.ancestry).all()]
- return dict(page='index', content=str(ancestries))
+ return self.output(content=str(ancestries))
diff --git a/ttfrog/webserver/widgets.py b/ttfrog/webserver/widgets.py
index 5cb71ea..61126f4 100644
--- a/ttfrog/webserver/widgets.py
+++ b/ttfrog/webserver/widgets.py
@@ -1,13 +1,21 @@
-import tw2.core
+import tw2.core as twc
import tw2.forms
-from tg import lurl
+from ttfrog.db import db
class CharacterSheet(tw2.forms.Form):
- class child(tw2.forms.TableLayout):
- name = tw2.forms.TextField()
- level = tw2.forms.TextField()
- ancestry_name = tw2.forms.TextField(label='Ancestry')
- id = tw2.forms.HiddenField()
-
action = ''
+
+ class child(tw2.forms.TableLayout):
+ name = tw2.forms.TextField(validator=twc.Required)
+ level = tw2.forms.SingleSelectField(
+ prompt_text=None,
+ options=range(1, 21),
+ validator=twc.validation.IntValidator(min=1, max=20)
+ )
+ ancestry_name = tw2.forms.SingleSelectField(
+ label='Ancestry',
+ prompt_text=None,
+ options=twc.Deferred(lambda: [a.name for a in db.query(db.ancestry)]),
+ validator=twc.Required,
+ )