Adding slugs, refactoring

This commit is contained in:
evilchili 2024-01-28 22:14:50 -08:00
parent 64451ddf8b
commit 8f17ddfb05
10 changed files with 151 additions and 58 deletions

View File

@ -1,5 +1,15 @@
<div style='float:left; max-width:20%'>
<ul>
{% for char in all_characters %}
<li><a href="/sheet/{{char['slug']}}/{{char['name']}}">{{ char['name'] }}</a></li>
{% endfor %}
</div>
<div>
<h1>{{ character.name }}</h1> <h1>{{ character.name }}</h1>
{{ form.display(value=character) }} {{ form.display(value=character) }}
{{ flash }}
<pre> <pre>
{{ character }} {{ character }}
</pre> </pre>
</div>

View File

@ -1,5 +1,3 @@
import base64
import hashlib
import logging import logging
from ttfrog.db import db, session 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(): def bootstrap():
""" """
Initialize the database with source data. Idempotent; will skip anything that already exists. 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.") logging.debug("No bootstrap data for table {table_name}; skipping.")
continue continue
for rec in data[table_name]: 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") stmt = table.insert().values(**rec).prefix_with("OR IGNORE")
result = session.execute(stmt) result, error = db.execute(stmt)
session.commit() if error:
last_id = result.inserted_primary_key[0] raise RuntimeError(error)
if last_id == 0:
rec['id'] = result.inserted_primary_key[0]
if rec['id'] == 0:
logging.info(f"Skipped existing {table_name} {rec}") logging.info(f"Skipped existing {table_name} {rec}")
else: continue
logging.info(f"Created {table_name} {result.inserted_primary_key[0]}: {rec}")
if 'slug' in table.columns:
rec['slug'] = db.slugify(rec)
db.update(table, **rec)
logging.info(f"Created {table_name} {rec}")

View File

@ -1,8 +1,12 @@
from functools import cached_property import base64
import hashlib
import logging import logging
from functools import cached_property
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.exc import IntegrityError
from ttfrog.path import database from ttfrog.path import database
from ttfrog.db.schema import metadata from ttfrog.db.schema import metadata
@ -32,17 +36,39 @@ class SQLDatabaseManager:
def query(self, *args, **kwargs): def query(self, *args, **kwargs):
return self.DBSession.query(*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): def update(self, table, **kwargs):
stmt = table.update().values(**kwargs) primary_key = kwargs.pop('id')
logging.debug(stmt) stmt = table.update().values(**kwargs).where(table.columns.id == primary_key)
result = self.DBSession.execute(stmt) return self.execute(stmt)
self.DBSession.commit()
return result
def init_model(self, engine=None): def init_model(self, engine=None):
metadata.create_all(bind=engine or self.engine) metadata.create_all(bind=engine or self.engine)
return self.DBSession 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): def __getattr__(self, name: str):
try: try:
return self.tables[name] return self.tables[name]

View File

@ -14,7 +14,8 @@ metadata = MetaData()
Ancestry = Table( Ancestry = Table(
"ancestry", "ancestry",
metadata, 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("slug", String, index=True, unique=True),
Column("description", UnicodeText), Column("description", UnicodeText),
) )

View File

@ -9,7 +9,7 @@ from wsgiref.simple_server import make_server
import webhelpers2 import webhelpers2
import tw2.core import tw2.core
from ttfrog.webserver.controllers import RootController from ttfrog.webserver.controllers.root import RootController
from ttfrog.db import db from ttfrog.db import db
import ttfrog.path import ttfrog.path

View File

@ -1,3 +0,0 @@
from .root import RootController
__ALL__ = [RootController]

View File

@ -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,
)

View File

@ -1,29 +1,72 @@
import base64
import hashlib
import logging
from tg import expose 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 import db
from ttfrog.db.schema import Character from ttfrog.db.schema import Character
from ttfrog.webserver.controllers.base import BaseController
from ttfrog.webserver.widgets import CharacterSheet from ttfrog.webserver.widgets import CharacterSheet
class CharacterSheetController(TGController): class CharacterSheetController(BaseController):
@expose() @expose()
def _lookup(self, *parts): 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): def __init__(self, slug: str):
super().__init__()
self.character = dict() self.character = dict()
if slug: 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 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') @expose('character_sheet.html')
def _default(self, *args, **kwargs): @validate(form=CharacterSheet)
if kwargs: def _default(self, *args, **fields):
db.update(Character, **kwargs) if fields:
self.load(self.character['slug']) return self.save(fields)
return dict(page='sheet', form=CharacterSheet, character=self.character) return self.output(
form=CharacterSheet,
character=self.character,
all_characters=self.all_characters,
)

View File

@ -1,18 +1,16 @@
from tg import expose from tg import expose
from tg import TGController
from tg import tmpl_context
from ttfrog.db import db from ttfrog.db import db
from ttfrog.webserver.controllers.base import BaseController
from ttfrog.webserver.controllers.character_sheet import CharacterSheetController from ttfrog.webserver.controllers.character_sheet import CharacterSheetController
class RootController(TGController): class RootController(BaseController):
# serve character sheet interface on /sheet
sheet = CharacterSheetController() sheet = CharacterSheetController()
def _before(self, *args, **kwargs):
tmpl_context.project_name = 'TableTop Frog'
@expose('index.html') @expose('index.html')
def index(self): def index(self):
ancestries = [row._mapping for row in db.query(db.ancestry).all()] ancestries = [row._mapping for row in db.query(db.ancestry).all()]
return dict(page='index', content=str(ancestries)) return self.output(content=str(ancestries))

View File

@ -1,13 +1,21 @@
import tw2.core import tw2.core as twc
import tw2.forms import tw2.forms
from tg import lurl from ttfrog.db import db
class CharacterSheet(tw2.forms.Form): 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 = '' 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,
)