Adding slugs, refactoring
This commit is contained in:
parent
64451ddf8b
commit
8f17ddfb05
|
@ -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>
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
from .root import RootController
|
|
||||||
|
|
||||||
__ALL__ = [RootController]
|
|
19
ttfrog/webserver/controllers/base.py
Normal file
19
ttfrog/webserver/controllers/base.py
Normal 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,
|
||||||
|
)
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user