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>
{{ form.display(value=character) }}
{{ flash }}
<pre>
{{ character }}
</pre>
</div>

View File

@ -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}")

View File

@ -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 update(self, table, **kwargs):
stmt = table.update().values(**kwargs)
logging.debug(stmt)
result = self.DBSession.execute(stmt)
def execute(self, statement) -> tuple:
logging.debug(statement)
result = None
error = None
try:
result = self.DBSession.execute(statement)
self.DBSession.commit()
return result
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):
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]

View File

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

View File

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

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

View File

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

View File

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