rewrite using pyramid and wtforms
This commit is contained in:
parent
5faf5c97c1
commit
3444f83c91
|
@ -10,19 +10,16 @@ packages = [
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.10"
|
python = "^3.10"
|
||||||
TurboGears2 = "^2.4.3"
|
|
||||||
sqlalchemy = "^2.0.25"
|
|
||||||
tgext-admin = "^0.7.4"
|
|
||||||
webhelpers2 = "^2.0"
|
|
||||||
typer = "^0.9.0"
|
|
||||||
python-dotenv = "^0.21.0"
|
python-dotenv = "^0.21.0"
|
||||||
|
typer = "^0.9.0"
|
||||||
rich = "^13.7.0"
|
rich = "^13.7.0"
|
||||||
jinja2 = "^3.1.3"
|
sqlalchemy = "^2.0.25"
|
||||||
tw2-forms = "^2.2.6"
|
pyramid = "^2.0.2"
|
||||||
mako = "^1.3.0"
|
pyramid-tm = "^2.5"
|
||||||
|
pyramid-jinja2 = "^2.10"
|
||||||
#"tg.devtools" = "^2.4.3"
|
pyramid-sqlalchemy = "^1.6"
|
||||||
#repoze-who = "^3.0.0"
|
wtforms-sqlalchemy = "^0.4.1"
|
||||||
|
transaction = "^4.0"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
@ -32,5 +29,3 @@ build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
ttfrog = "ttfrog.cli:app"
|
ttfrog = "ttfrog.cli:app"
|
||||||
|
|
||||||
|
|
||||||
|
|
13
ttfrog/assets/templates/base.html
Normal file
13
ttfrog/assets/templates/base.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{{ config['project_name'] }}{% block title %}{% endblock %}</title>
|
||||||
|
<meta name="og:provider_name" content="{{ config['project_name'] }}">
|
||||||
|
<link rel='stylesheet' href="{{config['static_url']}}/styles.css" />
|
||||||
|
{% block headers %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
{% block debug %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,15 +1,35 @@
|
||||||
<div style='float:left; max-width:20%'>
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div style='float:left; max-width:20%;height: 90%'>
|
||||||
<ul>
|
<ul>
|
||||||
{% for char in all_characters %}
|
{% for char in all_characters %}
|
||||||
<li><a href="/sheet/{{char['slug']}}/{{char['name']}}">{{ char['name'] }}</a></li>
|
<li><a href="/sheet/{{char['slug']}}/{{char['name']}}">{{ char['name'] }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1>{{ character.name }}</h1>
|
<h1>{{ record.name }}</h1>
|
||||||
{{ form.display(value=character) }}
|
|
||||||
{{ flash }}
|
|
||||||
|
|
||||||
<pre>
|
<form name="character_sheet" method="post" novalidate class="form">
|
||||||
{{ character }}
|
{{ form.csrf_token }}
|
||||||
</pre>
|
|
||||||
|
{% if 'process' in form.errors %}
|
||||||
|
Error: {{ form.errors['process'] |join(',') }}
|
||||||
|
{% endif %}
|
||||||
|
<ul>
|
||||||
|
<li>{{form.name.label}}: {{ form.name }} {{form.errors['name'] | join(',') }}</li>
|
||||||
|
<li>{{form.level.label}}: {{ form.level }} {{form.errors['level'] | join(',') }}<//li>
|
||||||
|
</ul>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block debug %}
|
||||||
|
<h2>Debug</h2>
|
||||||
|
<h3>Record</h3>
|
||||||
|
<pre>{{ record }}</pre>
|
||||||
|
<h3>Config</h3>
|
||||||
|
<pre>{{ config }}</pre>
|
||||||
|
{% endblock %}
|
||||||
|
|
55
ttfrog/db/base.py
Normal file
55
ttfrog/db/base.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
from pyramid_sqlalchemy import BaseObject
|
||||||
|
from wtforms import validators
|
||||||
|
|
||||||
|
|
||||||
|
class IterableMixin:
|
||||||
|
"""
|
||||||
|
Allows for iterating over Model objects' column names and values
|
||||||
|
"""
|
||||||
|
def __iter__(self):
|
||||||
|
values = vars(self)
|
||||||
|
for attr in self.__mapper__.columns.keys():
|
||||||
|
if attr in values:
|
||||||
|
yield attr, values[attr]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__}: {str(dict(self))}"
|
||||||
|
|
||||||
|
|
||||||
|
class FormValidatorMixin:
|
||||||
|
"""
|
||||||
|
Add form validation capabilities using the .info attributes of columns.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# column.info could contain any of these keywords. define the list of validators that should apply
|
||||||
|
# whenever we encounter one such keyword.
|
||||||
|
_validators_by_keyword = {
|
||||||
|
'min': [validators.NumberRange],
|
||||||
|
'max': [validators.NumberRange],
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, form):
|
||||||
|
for name, column in cls.__mapper__.columns.items():
|
||||||
|
if name not in form._fields:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# step through the info keywords and create a deduped list of validator classes that
|
||||||
|
# should apply to this form field. This prevents adding unnecessary copies of the same
|
||||||
|
# validator when two or more keywords map to the same one.
|
||||||
|
extras = set()
|
||||||
|
for key in column.info.keys():
|
||||||
|
for val in cls._validators_by_keyword.get(key, []):
|
||||||
|
extras.add(val)
|
||||||
|
|
||||||
|
# Add an instance of every unique validator for this column to the associated form field.
|
||||||
|
form._fields[name].validators.extend([v(**column.info) for v in extras])
|
||||||
|
|
||||||
|
# return the results of the form validation,.
|
||||||
|
return form.validate()
|
||||||
|
|
||||||
|
|
||||||
|
# class Table(*Bases):
|
||||||
|
Bases = [BaseObject, IterableMixin, FormValidatorMixin]
|
|
@ -5,6 +5,7 @@ from ttfrog.db.manager import db
|
||||||
from ttfrog.db import schema
|
from ttfrog.db import schema
|
||||||
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.inspection import inspect
|
||||||
|
|
||||||
# move this to json or whatever
|
# move this to json or whatever
|
||||||
data = {
|
data = {
|
||||||
|
@ -28,16 +29,14 @@ def bootstrap():
|
||||||
model = getattr(schema, table)
|
model = getattr(schema, table)
|
||||||
|
|
||||||
for rec in records:
|
for rec in records:
|
||||||
with transaction.manager as tx:
|
|
||||||
obj = model(**rec)
|
obj = model(**rec)
|
||||||
|
try:
|
||||||
|
with db.transaction():
|
||||||
db.session.add(obj)
|
db.session.add(obj)
|
||||||
obj.slug = db.slugify(rec)
|
obj.slug = db.slugify(rec)
|
||||||
try:
|
|
||||||
tx.commit()
|
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
tx.abort()
|
|
||||||
if 'UNIQUE constraint failed' in str(e):
|
if 'UNIQUE constraint failed' in str(e):
|
||||||
logging.info(f"Skipping existing {table} {rec}")
|
logging.info(f"Skipping existing {table} {obj}")
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
logging.info(f"Created {table} {rec}")
|
logging.info(f"Created {table} {obj}")
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import transaction
|
import transaction
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from pyramid_sqlalchemy import Session
|
from pyramid_sqlalchemy import Session
|
||||||
|
@ -10,7 +10,7 @@ from pyramid_sqlalchemy import init_sqlalchemy
|
||||||
from pyramid_sqlalchemy import metadata as _metadata
|
from pyramid_sqlalchemy import metadata as _metadata
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.exc import IntegrityError
|
# from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from ttfrog.path import database
|
from ttfrog.path import database
|
||||||
import ttfrog.db.schema
|
import ttfrog.db.schema
|
||||||
|
@ -30,7 +30,7 @@ class SQLDatabaseManager:
|
||||||
def engine(self):
|
def engine(self):
|
||||||
return create_engine(self.url)
|
return create_engine(self.url)
|
||||||
|
|
||||||
@cached_property
|
@property
|
||||||
def session(self):
|
def session(self):
|
||||||
return Session
|
return Session
|
||||||
|
|
||||||
|
@ -42,31 +42,19 @@ class SQLDatabaseManager:
|
||||||
def tables(self):
|
def tables(self):
|
||||||
return dict((t.name, t) for t in self.metadata.sorted_tables)
|
return dict((t.name, t) for t in self.metadata.sorted_tables)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def transaction(self):
|
||||||
|
with transaction.manager as tm:
|
||||||
|
yield tm
|
||||||
|
try:
|
||||||
|
tm.commit()
|
||||||
|
except Exception:
|
||||||
|
tm.abort()
|
||||||
|
raise
|
||||||
|
|
||||||
def query(self, *args, **kwargs):
|
def query(self, *args, **kwargs):
|
||||||
return self.session.query(*args, **kwargs)
|
return self.session.query(*args, **kwargs)
|
||||||
|
|
||||||
def execute(self, statement) -> tuple:
|
|
||||||
logging.info(statement)
|
|
||||||
result = None
|
|
||||||
error = None
|
|
||||||
try:
|
|
||||||
with transaction.manager as tx:
|
|
||||||
result = self.session.execute(statement)
|
|
||||||
tx.commit()
|
|
||||||
except IntegrityError as exc:
|
|
||||||
logging.error(exc)
|
|
||||||
error = "I AM ERROR."
|
|
||||||
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 slugify(self, rec: dict) -> str:
|
def slugify(self, rec: dict) -> str:
|
||||||
"""
|
"""
|
||||||
Create a uniquish slug from a dictionary.
|
Create a uniquish slug from a dictionary.
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
from sqlalchemy import MetaData
|
|
||||||
from sqlalchemy import Table
|
|
||||||
from sqlalchemy import Column
|
from sqlalchemy import Column
|
||||||
from sqlalchemy import Integer
|
from sqlalchemy import Integer
|
||||||
from sqlalchemy import String
|
from sqlalchemy import String
|
||||||
from sqlalchemy import UnicodeText
|
|
||||||
from sqlalchemy import ForeignKey
|
from sqlalchemy import ForeignKey
|
||||||
from sqlalchemy import CheckConstraint
|
from sqlalchemy import CheckConstraint
|
||||||
# from sqlalchemy import PrimaryKeyConstraint
|
# from sqlalchemy import PrimaryKeyConstraint
|
||||||
# from sqlalchemy import DateTime
|
# from sqlalchemy import DateTime
|
||||||
|
|
||||||
from pyramid_sqlalchemy import BaseObject
|
from ttfrog.db.base import Bases
|
||||||
|
|
||||||
class Ancestry(BaseObject):
|
|
||||||
|
class Ancestry(*Bases):
|
||||||
__tablename__ = "ancestry"
|
__tablename__ = "ancestry"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
@ -19,17 +17,17 @@ class Ancestry(BaseObject):
|
||||||
slug = Column(String, index=True, unique=True)
|
slug = Column(String, index=True, unique=True)
|
||||||
|
|
||||||
|
|
||||||
class Character(BaseObject):
|
class Character(*Bases):
|
||||||
__tablename__ = "character"
|
__tablename__ = "character"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
slug = Column(String, index=True, unique=True)
|
slug = Column(String, index=True, unique=True)
|
||||||
ancestry = Column(String, ForeignKey("ancestry.name"))
|
ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False)
|
||||||
name = Column(String)
|
name = Column(String(255), nullable=False)
|
||||||
level = Column(Integer, CheckConstraint('level > 0 AND level <= 20'))
|
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
|
||||||
str = Column(Integer, CheckConstraint('str >=0'))
|
str = Column(Integer, info={'min': 1})
|
||||||
dex = Column(Integer, CheckConstraint('dex >=0'))
|
dex = Column(Integer, info={'min': 1})
|
||||||
con = Column(Integer, CheckConstraint('con >=0'))
|
con = Column(Integer, info={'min': 1})
|
||||||
int = Column(Integer, CheckConstraint('int >=0'))
|
int = Column(Integer, info={'min': 1})
|
||||||
wis = Column(Integer, CheckConstraint('wis >=0'))
|
wis = Column(Integer, info={'min': 1})
|
||||||
cha = Column(Integer, CheckConstraint('cha >=0'))
|
cha = Column(Integer, info={'min': 1})
|
||||||
|
|
|
@ -10,9 +10,14 @@ from ttfrog.webserver.routes import routes
|
||||||
def configuration():
|
def configuration():
|
||||||
config = Configurator(settings={
|
config = Configurator(settings={
|
||||||
'sqlalchemy.url': db.url,
|
'sqlalchemy.url': db.url,
|
||||||
|
'jinja2.directories': 'ttfrog.assets:templates/'
|
||||||
})
|
})
|
||||||
config.include('pyramid_tm')
|
config.include('pyramid_tm')
|
||||||
config.include('pyramid_sqlalchemy')
|
config.include('pyramid_sqlalchemy')
|
||||||
|
config.include('pyramid_jinja2')
|
||||||
|
config.add_static_view(name='/static', path='ttfrog.assets:static/')
|
||||||
|
config.add_jinja2_renderer('.html', settings_prefix='jinja2.')
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
from .base import BaseController
|
||||||
|
from .character_sheet import CharacterSheet
|
||||||
|
|
||||||
|
__all__ = [BaseController, CharacterSheet]
|
|
@ -1,19 +1,74 @@
|
||||||
import inspect
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from tg import flash
|
from wtforms_sqlalchemy.orm import model_form
|
||||||
from tg import TGController
|
|
||||||
from tg import tmpl_context
|
from ttfrog.db.manager import db
|
||||||
from markupsafe import Markup
|
|
||||||
|
|
||||||
|
|
||||||
class BaseController(TGController):
|
class BaseController:
|
||||||
|
model = None
|
||||||
|
|
||||||
def _before(self, *args, **kwargs):
|
def __init__(self, request):
|
||||||
tmpl_context.project_name = 'TableTop Frog'
|
self.request = request
|
||||||
|
self.record = None
|
||||||
|
self.attrs = defaultdict(str)
|
||||||
|
self.configure()
|
||||||
|
if self.model:
|
||||||
|
self.model_form = model_form(self.model, db_session=db.session)
|
||||||
|
|
||||||
|
# load this from dotenv or something
|
||||||
|
self.config = {
|
||||||
|
'static_url': '/static',
|
||||||
|
'project_name': 'TTFROG'
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.load_from_id()
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_id(self):
|
||||||
|
if not self.request.POST['id']:
|
||||||
|
return
|
||||||
|
self.record = db.query(self.model).get(self.request.POST['id'])
|
||||||
|
|
||||||
|
def form(self) -> str:
|
||||||
|
# no model? no form.
|
||||||
|
if not self.model:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
# no user submission to process
|
||||||
|
if self.request.method != 'POST':
|
||||||
|
return self.model_form(obj=self.record)
|
||||||
|
|
||||||
|
# process submission
|
||||||
|
form = self.model_form(self.request.POST, obj=self.record)
|
||||||
|
if self.model.validate(form):
|
||||||
|
form.populate_obj(self.record)
|
||||||
|
error = self.save_changes()
|
||||||
|
if error:
|
||||||
|
form.errors['process'] = error
|
||||||
|
return form
|
||||||
|
|
||||||
|
def save_changes(self):
|
||||||
|
try:
|
||||||
|
with db.transaction():
|
||||||
|
for (key, val) in self.request.POST.items():
|
||||||
|
if hasattr(self.record, key):
|
||||||
|
setattr(self.record, key, val)
|
||||||
|
except Exception as e:
|
||||||
|
return e
|
||||||
|
return None
|
||||||
|
|
||||||
def output(self, **kwargs) -> dict:
|
def output(self, **kwargs) -> dict:
|
||||||
return dict(
|
return dict(
|
||||||
page=inspect.stack()[1].function,
|
config=self.config,
|
||||||
flash=Markup(flash.render('flash', use_js=False)),
|
request=self.request,
|
||||||
|
record=self.record,
|
||||||
|
form=self.form(),
|
||||||
|
**self.attrs,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def response(self):
|
||||||
|
return self.output()
|
||||||
|
|
|
@ -1,72 +1,20 @@
|
||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from tg import expose
|
from ttfrog.webserver.controllers import BaseController
|
||||||
from tg import flash
|
from ttfrog.db.manager import db
|
||||||
from tg import validate
|
|
||||||
from tg.controllers.util import redirect
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class CharacterSheetController(BaseController):
|
class CharacterSheet(BaseController):
|
||||||
@expose()
|
model = Character
|
||||||
def _lookup(self, *parts):
|
|
||||||
slug = parts[0] if parts else ''
|
|
||||||
return FormController(slug), parts[1:] if len(parts) > 1 else []
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
class FormController(BaseController):
|
self.attrs['all_characters'] = db.query(Character).all()
|
||||||
|
slug = self.request.matchdict.get('slug', None)
|
||||||
def __init__(self, slug: str):
|
|
||||||
super().__init__()
|
|
||||||
self.character = dict()
|
|
||||||
if slug:
|
if slug:
|
||||||
self.load_from_slug(slug)
|
try:
|
||||||
|
self.record = db.query(Character).filter(Character.slug == slug)[0]
|
||||||
@property
|
except IndexError:
|
||||||
def uri(self):
|
logging.warning(f"Could not load record with slug {slug}")
|
||||||
if self.character:
|
|
||||||
return f"/sheet/{self.character['slug']}/{self.character['name']}"
|
|
||||||
else:
|
else:
|
||||||
return None
|
self.load_from_id()
|
||||||
|
|
||||||
@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')
|
|
||||||
@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,
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
from tg import expose
|
|
||||||
|
|
||||||
from ttfrog.db import db
|
|
||||||
from ttfrog.webserver.controllers.base import BaseController
|
|
||||||
from ttfrog.webserver.controllers.character_sheet import CharacterSheetController
|
|
||||||
|
|
||||||
|
|
||||||
class RootController(BaseController):
|
|
||||||
|
|
||||||
# serve character sheet interface on /sheet
|
|
||||||
sheet = CharacterSheetController()
|
|
||||||
|
|
||||||
@expose('index.html')
|
|
||||||
def index(self):
|
|
||||||
ancestries = [row._mapping for row in db.query(db.ancestry).all()]
|
|
||||||
return self.output(content=str(ancestries))
|
|
|
@ -1,2 +1,3 @@
|
||||||
def routes(config):
|
def routes(config):
|
||||||
config.add_route('index', '/')
|
config.add_route('index', '/')
|
||||||
|
config.add_route('sheet', '/sheet/{slug}/{name}', factory='ttfrog.webserver.controllers.CharacterSheet')
|
||||||
|
|
|
@ -8,3 +8,9 @@ from ttfrog.db.schema import Ancestry
|
||||||
def index(request):
|
def index(request):
|
||||||
ancestries = [a.name for a in db.session.query(Ancestry).all()]
|
ancestries = [a.name for a in db.session.query(Ancestry).all()]
|
||||||
return Response(','.join(ancestries))
|
return Response(','.join(ancestries))
|
||||||
|
|
||||||
|
|
||||||
|
@view_config(route_name='sheet', renderer='character_sheet.html')
|
||||||
|
def sheet(request):
|
||||||
|
sheet = request.context
|
||||||
|
return sheet.response()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user