layout updates, added json views, fixed relationships in schema

This commit is contained in:
evilchili 2024-02-16 01:19:25 -08:00
parent 1baf73a338
commit e231828425
13 changed files with 320 additions and 88 deletions

View File

@ -24,6 +24,7 @@ unicode-slugify = "^0.1.5"
nanoid = "^2.0.0" nanoid = "^2.0.0"
nanoid-dictionary = "^2.4.0" nanoid-dictionary = "^2.4.0"
wtforms-alchemy = "^0.18.0" wtforms-alchemy = "^0.18.0"
sqlalchemy-serializer = "^1.4.1"
[build-system] [build-system]

View File

@ -4,6 +4,19 @@ body {
margin: 0; margin: 0;
} }
.disabled {
position : relative;
opacity: 0.3;
}
.disabled:after {
position :absolute;
left : 0;
top : 0;
width : 100%;
height : 100%;
content :' ';
}
#content { #content {
margin: 1rem auto; margin: 1rem auto;
@ -35,79 +48,161 @@ ul.nav li {
padding: 0 0.5rem; padding: 0 0.5rem;
} }
#character_sheet { #sheet_container {
width:50%;
display: block; display: block;
} }
#character_sheet .statblock { #character_sheet {
width: 100%; width: 100%;
margin-bottom:3rem; margin-bottom:3rem;
display: grid; display: grid;
grid-template-columns: auto 250px;
grid-gap: 1rem; grid-gap: 1rem;
grid-template-columns: min-content max-content;
} }
#character_sheet .banner { #sheet_container .banner {
display: grid; display: grid;
grid-template-columns: 64px 1fr; grid-template-columns: 64px 1fr;
grid-gap: 1rem; grid-gap: 1rem;
} }
#character_sheet .banner #portrait { #sheet_container .banner #portrait {
width: 64px; width: 64px;
height: 64px; height: 64px;
background: #e7e7e7; background: #e7e7e7;
} }
#controls {
float: right;
display: inline-block;
}
#character_sheet h1 { .temp_hp input {
font-size: 0.75rem !important;
}
#sheet_container h1 {
margin: 0; margin: 0;
} }
#character_sheet .sidebar { #sheet_container .sidebar {
min-height: 300px; grid-column-start: 2;
grid-row-start: 1;
} }
#character_sheet input, #sheet_container .sidebar .card {
#character_sheet select, margin-bottom: 1rem;
#character_sheet textarea { }
#sheet_container .sidebar ul {
list-style-type: none;
margin: 0;
padding: 0;
}
#sheet_container .sidebar ul > li {
margin: 0;
padding: 0;
}
#hp {
grid-row-start: 1;
grid-column: 7;
grid-column-end: 9;
}
#saves {
grid-row-start: 2;
grid-column: 3;
grid-column-start: span 2;
}
#proficiency {
grid-row-start: 2;
grid-column: 5;
}
#initiative {
grid-row-start: 2;
grid-column: 6;
}
#ac {
grid-row-start: 2;
grid-column: 7;
}
#speed {
grid-row-start: 2;
grid-column: 8;
}
#skills {
grid-row-start: 2;
grid-row-end: 50;
grid-column: 1;
grid-column-start: span 2;
text-align:left;
}
#actions {
grid-row-start: 3;
grid-column: 4;
grid-column-start: span 6;
}
table {
display: grid;
grid-template-columns: minmax(50px, 150px) 1fr;
grid-gap: 0rem;
}
table th {
grid-column-start: span 4;
white-space: nowrap;
padding-right: 1rem;
text-align: left;
}
table td {
padding-right: 1rem;
white-space: nowrap;
}
.note {
font-size: 0.75em;
font-style: italic;
}
#sheet_container input,
#sheet_container select,
#sheet_container textarea {
font-weight: bold; font-weight: bold;
border: 0; border: 0;
} }
#character_sheet input#name { #sheet_container input#name {
font-size: 1.5rem; font-size: 2.0rem;
font-weight: bold; font-weight: bold;
width: 100%; width: 100%;
} }
.stats .cards { .stats {
margin-top: 1rem;
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(8, minmax(6rem, 1fr));
grid-auto-rows: auto;
grid-gap: 1rem; grid-gap: 1rem;
} }
.sidebar .cards { .card {
margin-top: 1rem;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto;
grid-gap: 1rem;
text-align: center;
}
.cards .card {
border: 2px solid #e7e7e7; border: 2px solid #e7e7e7;
border-radius: 4px; border-radius: 4px;
padding: .5rem; padding: .5rem;
text-align: center;
} }
.card .label { .label {
text-align: center; text-align: center;
text-transform: uppercase; text-transform: uppercase;
font-size: 0.75rem;
} }
.card input { .card input {

View File

@ -58,6 +58,6 @@ function setSpellSaveDC() {
stats.forEach(applyStatModifiers); stats.forEach(applyStatModifiers);
stats.forEach(setStatBonus); stats.forEach(setStatBonus);
setProficiencyBonus(); setProficiencyBonus();
setSpellSaveDC(); // setSpellSaveDC();
})(); })();

View File

@ -1,3 +1,4 @@
{% from "list.html" import build_list %}
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
@ -14,6 +15,7 @@
{% block headers %}{% endblock %} {% block headers %}{% endblock %}
</head> </head>
<body> <body>
{{ build_list(c) }}
<div id='content'> <div id='content'>
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>

View File

@ -1,66 +1,153 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "list.html" import build_list %} {% set DISABLED = False if c.record.id else True %}
{% macro field(name, disabled=False) %}
{% set default_value = c.record[name] if c.record.id else c.form[name].default %}
{{ c.form[name](disabled=disabled, **{'data-initial_value': default_value}) }}
{% endmacro %}
{% block content %} {% block content %}
{{ build_list(c) }} <div id='sheet_container'>
<div id='character_sheet'>
<form name="character_sheet" method="post" novalidate class="form"> <form name="character_sheet" method="post" novalidate class="form">
<div class='banner'> <div class='banner'>
<div><img id='portrait' /></div> <div><img id='portrait' /></div>
<div> <div>
{{ c.form.name }} {{ field('name') }}
{{ c.form.ancestry }} {{ c.record.character_class|join(' / ') }} &nbsp; Level {{ c.form.level }} {{ field('ancestry') }} {{ c.record.character_class|join(' / ') }} &nbsp; Level {{ field('level') }}
<div id='controls'>
{{ c.form.save }} &nbsp; {{ c.form.delete }}
</div> </div>
</div> </div>
<div class='statblock'> </div>
</div>
<div id='character_sheet' {% if not c.record.id %}class='disabled'{% endif %} >
<div class='stats'> <div class='stats'>
<div class='cards'>
{% for stat in ['str', 'dex', 'con', 'int', 'wis', 'cha'] %} {% for stat in ['str', 'dex', 'con', 'int', 'wis', 'cha'] %}
<div class='card'> <div class='card'>
<div class='label'>{{ c.form[stat].label }}</div> <div class='label'>{{ c.form[stat].label }}</div>
{{ c.form[stat] }} {{ field(stat, DISABLED) }}
<div id='{{stat}}_bonus'></div> <div id='{{stat}}_bonus'></div>
</div> </div>
{% endfor %} {% endfor %}
<div class='card'> <div id='hp' class='card'>
<div class='label'>AC</div> <div class='label'>HP</div>
{{ c.form.armor_class }} {{ field('hit_points', DISABLED) }} / {{ field('max_hit_points', DISABLED) }}
<div id='temp_hp'>
<span class='label'>TEMP</span> {{ field('temp_hit_points', DISABLED) }}
</div> </div>
</div> </div>
<ul> <div id='skills'>
<li>Initiative: <span id='initiative'>3</span></li> <div class='label'>Skills</div>
<li>Proficiency Bonus: <span id='proficiency_bonus'></span></li> <table>
<li>Spell Save DC: <span id='spell_save_dc'></span></li> {% for skill in c.record.skills %}
<li>Saving Throws: <span id='saving_throws'>{{ c.record.saving_throws |join(', ') }}</span></li> <tr><td>{{ skill }}</td><td>3</td></tr>
<li>Skills: <span id='skills'>{{ c.record.skills |join(', ') }}</span></li>
{% for field in c.form %}
{% if field.name in ['proficiencies', 'speed', 'passive_perception', 'passive_insight', 'passive_investigation'] %}
<li>{{ field.label }}: {{ field }} {{ field.errors|join(',') }}</li>
{% endif %}
{% endfor %} {% endfor %}
</table>
</div>
<div id='saves' class='card'>
<div class='label'>Saving Throws</div>
{% for save in c.record.saving_throws %}
{{ save }} 3&nbsp;
{% endfor %}
</div>
<div id='proficiency' class='card'>
<div class='label'>PROF</div>
<div id='proficiency_bonus'></div>
<div class='label'>BONUS</div>
</div>
<div id="ac" class='card'>
<div class='label'>Armor</div>
{{ field('armor_class', DISABLED) }}
<div class='label'>Class</div>
</div>
<div id='initiative' class='card'>
<div class='label'>Initiative</div>
<span id='initiative_bonus'>3 </span>
<div class='label'>Bonus</div>
</div>
<div id='speed' class='card'>
<div class='label'>Speed</div>
{{ field('speed', DISABLED) }}
</div>
<div id="actions" class='card'>
<table>
<tr>
<td class='label' colspan='2'>Actions</td>
<td class='label'>To Hit</td>
<td class='label'>Range</td>
<td class='label'>Targets</td>
<td class='label'>Damage</td>
</tr>
<tr>
<th>Attack</th>
<td>Dagger</td>
<td>+7</td>
<td>5</td>
<td>1</td>
<td>1d4+3 slashing</td>
</tr>
<tr>
<th>Attack</th>
<td>Sabetha's Fans</td>
<td>+7</td>
<td>5</td>
<td>1</td>
<td>2d6 slashing</td>
</tr>
<tr>
<th>Spell</th>
<td>Eldritch Blast</td>
<td>+5</td>
<td>120</td>
<td>1</td>
<td>1d10 force</td>
</tr>
<tr>
<td class='label' colspan='2'>Bonus Actions</td>
<td class='label'>To Hit</td>
<td class='label'>Range</td>
<td class='label'>Targets</td>
<td class='label'>Damage</td>
</tr>
</table>
<p>
<span class='note'>
Attack (1 per Action), Cast a Spell, Dash, Disengage, Dodge, Grapple,<br>Help, Hide, Improvise, Ready, Search, Shove, or Use an Object
</span>
</p>
</div>
</div>
<!-- SIDEBAR -->
<div class='sidebar'>
<div class='card'>
<div class='label'>Inspiration</div>
<ul>
</ul> </ul>
</div> </div>
<div class='sidebar'>
<div class='cards'>
<div class='card'> <div class='card'>
<div class='label'>HP</div> <div class='label'>Conditions</div>
{{ c.form.max_hit_points }} / {{ c.form.hit_points }} <ul>
</ul>
</div> </div>
<div class='card'> <div class='card'>
<div class='label'>TEMP HP</div> <div class='label'>Defenses</div>
{{ c.form.temp_hit_points }} <ul>
</div> <li>Vulnerable to Fire</li>
<li>Immune to Cold</li>
<li>Resistant to Poison</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
<hr> <hr>
{{ c.form.save }} &nbsp; {{ c.form.delete }}
</div>
{{ c.form.csrf_token }} {{ c.form.csrf_token }}
</form> </form>
@ -70,6 +157,8 @@
<div style='clear:both;display:block;'> <div style='clear:both;display:block;'>
<h2>Debug</h2> <h2>Debug</h2>
<code> <code>
{{ DISABLED }}
<code>
{{ c }} {{ c }}
</code> </code>
{% endblock %} {% endblock %}

View File

@ -1,8 +1,8 @@
import logging
import nanoid import nanoid
from nanoid_dictionary import human_alphabet from nanoid_dictionary import human_alphabet
from sqlalchemy import Column from sqlalchemy import Column
from sqlalchemy import String from sqlalchemy import String
from sqlalchemy import String
from pyramid_sqlalchemy import BaseObject from pyramid_sqlalchemy import BaseObject
from slugify import slugify from slugify import slugify
@ -31,9 +31,23 @@ class IterableMixin:
for attr in self.__mapper__.columns.keys(): for attr in self.__mapper__.columns.keys():
if attr in values: if attr in values:
yield attr, values[attr] yield attr, values[attr]
for relname in self.__mapper__.relationships.keys():
relvals = []
for rel in self.__getattribute__(relname):
try:
relvals.append({k: v for k, v in vars(rel).items() if not k.startswith('_')})
except TypeError:
relvals.append(rel)
yield relname, relvals
def __repr__(self): def __json__(self, request):
return f"{self.__class__.__name__}: {str(dict(self))}" serialized = dict()
for (key, value) in self:
try:
serialized[key] = getattr(self.value, '__json__')(request)
except AttributeError:
serialized[key] = value
return serialized
def multivalue_string_factory(name, column=Column(String), separator=';'): def multivalue_string_factory(name, column=Column(String), separator=';'):

View File

@ -62,7 +62,7 @@ data = {
'max_hit_points': 14, 'max_hit_points': 14,
'hit_points': 14, 'hit_points': 14,
'temp_hit_points': 0, 'temp_hit_points': 0,
'speed': '30 ft.', 'speed': 30,
'str': 16, 'str': 16,
'dex': 12, 'dex': 12,
'con': 18, 'con': 18,
@ -88,18 +88,32 @@ data = {
{ {
'id': 1, 'id': 1,
'ancestry_id': 1, 'ancestry_id': 1,
'description': '+1 to All Ability Scores', 'name': '+1 to All Ability Scores',
'level': 1,
},
{
'id': 2,
'ancestry_id': 2,
'name': 'Breath Weapon',
'level': 1, 'level': 1,
}, },
], ],
'Modifier': [ 'Modifier': [
# Humans
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'str'}, {'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'str'},
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'dex'}, {'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'dex'},
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'con'}, {'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'con'},
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'int'}, {'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'int'},
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'wis'}, {'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'wis'},
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'cha'}, {'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'cha'},
# Dragonborn
{'source_table_name': 'ancestry_trait', 'source_table_id': 2, 'value': '60', 'type': 'attribute ', 'target': 'Darkvision'},
{'source_table_name': 'ancestry_trait', 'source_table_id': 2, 'value': '+1', 'type': 'stat', 'target': ''},
{'source_table_name': 'ancestry_trait', 'source_table_id': 2, 'value': '+1', 'type': 'stat', 'target': ''},
# Fighting Style: Archery
{'source_table_name': 'class_attribute', 'source_table_id': 1, 'value': '+2', 'type': 'weapon ', 'target': 'ranged'}, {'source_table_name': 'class_attribute', 'source_table_id': 1, 'value': '+2', 'type': 'weapon ', 'target': 'ranged'},
], ],

View File

@ -7,6 +7,7 @@ from sqlalchemy import ForeignKey
from sqlalchemy import Enum from sqlalchemy import Enum
from sqlalchemy import Text from sqlalchemy import Text
from sqlalchemy import UniqueConstraint from sqlalchemy import UniqueConstraint
from sqlalchemy.orm import relationship
from ttfrog.db.base import Bases, BaseObject, IterableMixin from ttfrog.db.base import Bases, BaseObject, IterableMixin
from ttfrog.db.base import multivalue_string_factory from ttfrog.db.base import multivalue_string_factory
@ -17,9 +18,18 @@ STATS = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
CREATURE_TYPES = ['aberation', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'Giant', CREATURE_TYPES = ['aberation', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'Giant',
'humanoid', 'monstrosity', 'ooze', 'plant', 'undead'] 'humanoid', 'monstrosity', 'ooze', 'plant', 'undead']
class EnumField(enum.Enum):
"""
A serializable enum.
"""
def __json__(self, request):
return self.value
# enums for db schemas # enums for db schemas
StatsEnum = enum.Enum("StatsEnum", ((k, k) for k in STATS)) StatsEnum = EnumField("StatsEnum", ((k, k) for k in STATS))
CreatureTypesEnum = enum.Enum("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES)) CreatureTypesEnum = EnumField("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
CharacterClassMixin = multivalue_string_factory('character_class', Column(String, nullable=False)) CharacterClassMixin = multivalue_string_factory('character_class', Column(String, nullable=False))
SavingThrowsMixin = multivalue_string_factory('saving_throws') SavingThrowsMixin = multivalue_string_factory('saving_throws')
@ -50,6 +60,7 @@ class Ancestry(*Bases):
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, index=True, unique=True) name = Column(String, index=True, unique=True)
creature_type = Column(Enum(CreatureTypesEnum)) creature_type = Column(Enum(CreatureTypesEnum))
traits = relationship("AncestryTrait")
def __repr__(self): def __repr__(self):
return str(self.name) return str(self.name)
@ -59,13 +70,10 @@ class AncestryTrait(BaseObject, IterableMixin):
__tablename__ = "ancestry_trait" __tablename__ = "ancestry_trait"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False) ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False)
name = Column(String, nullable="False") name = Column(String, nullable=False)
description = Column(Text) description = Column(Text)
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}) level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
def __repr__(self):
return str(self.name)
class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin): class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
__tablename__ = "character_class" __tablename__ = "character_class"
@ -83,8 +91,8 @@ class ClassAttribute(BaseObject, IterableMixin):
__tablename__ = "class_attribute" __tablename__ = "class_attribute"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
character_class_id = Column(Integer, ForeignKey("character_class.id"), nullable=False) character_class_id = Column(Integer, ForeignKey("character_class.id"), nullable=False)
name = Column(String, nullable="False") name = Column(String, nullable=False)
value = Column(String, nullable="False") value = Column(String, nullable=False)
description = Column(Text) description = Column(Text)
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}) level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
@ -95,14 +103,14 @@ class ClassAttribute(BaseObject, IterableMixin):
class Character(*Bases, CharacterClassMixin, SavingThrowsMixin, SkillsMixin): class Character(*Bases, CharacterClassMixin, SavingThrowsMixin, SkillsMixin):
__tablename__ = "character" __tablename__ = "character"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False) ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False, default='human')
name = Column(String, default='New Character', nullable=False) name = Column(String, default='New Character', nullable=False)
level = Column(Integer, default=1, nullable=False, info={'min': 1, 'max': 20}) level = Column(Integer, default=1, nullable=False, info={'min': 1, 'max': 20})
armor_class = Column(Integer, default=10, nullable=False, info={'min': 1, 'max': 99}) armor_class = Column(Integer, default=10, nullable=False, info={'min': 1, 'max': 99})
hit_points = Column(Integer, default=1, nullable=False, info={'min': 0, 'max': 999}) hit_points = Column(Integer, default=1, nullable=False, info={'min': 0, 'max': 999})
max_hit_points = Column(Integer, default=1, nullable=False, info={'min': 0, 'max': 999}) max_hit_points = Column(Integer, default=1, nullable=False, info={'min': 0, 'max': 999})
temp_hit_points = Column(Integer, default=0, nullable=False, info={'min': 0}) temp_hit_points = Column(Integer, default=0, nullable=False, info={'min': 0, 'max': 999})
speed = Column(String, nullable=False, default="30 ft.") speed = Column(Integer, nullable=False, default=30, info={'min': 0, 'max': 99})
str = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30}) str = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
dex = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30}) dex = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
con = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30}) con = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})

View File

@ -1,4 +1,5 @@
from .base import BaseController from .base import BaseController
from .character_sheet import CharacterSheet from .character_sheet import CharacterSheet
from .json_data import JsonData
__all__ = [BaseController, CharacterSheet] __all__ = [BaseController, CharacterSheet, JsonData]

View File

@ -63,6 +63,8 @@ class BaseController:
def form(self): def form(self):
if not self.model: if not self.model:
return return
if not self.model_form:
return
if not self._form: if not self._form:
if self.request.POST: if self.request.POST:
self._form = self.model_form(self.request.POST, obj=self.record) self._form = self.model_form(self.request.POST, obj=self.record)
@ -106,8 +108,9 @@ class BaseController:
db.add(self.record) db.add(self.record)
logging.debug(f"Added {self.record = }") logging.debug(f"Added {self.record = }")
location = self.request.current_route_path() location = self.request.current_route_path()
if self.slug not in location: if self.record.slug not in location:
location = f"{location}/{self.record.uri}" location = f"{location}/{self.record.uri}"
logging.debug(f"Redirecting to {location}")
return HTTPFound(location=location) return HTTPFound(location=location)
def delete(self): def delete(self):

View File

@ -17,7 +17,7 @@ class CharacterForm(ModelForm):
save = SubmitField() save = SubmitField()
delete = SubmitField() delete = SubmitField()
ancestry = DeferredSelectField('Ancestry', model=Ancestry, validate_choice=True, widget=Select()) ancestry = DeferredSelectField('Ancestry', model=Ancestry, default='human', validate_choice=True, widget=Select())
character_class = DeferredSelectMultipleField( character_class = DeferredSelectMultipleField(
'CharacterClass', 'CharacterClass',

View File

@ -1,3 +1,4 @@
def routes(config): def routes(config):
config.add_route('index', '/') config.add_route('index', '/')
config.add_route('sheet', '/c{uri:.*}', factory='ttfrog.webserver.controllers.CharacterSheet') config.add_route('sheet', '/c{uri:.*}', factory='ttfrog.webserver.controllers.CharacterSheet')
config.add_route('data', '/_/{table_name}{uri:.*}', factory='ttfrog.webserver.controllers.JsonData')

View File

@ -17,3 +17,7 @@ def index(request):
@view_config(route_name='sheet', renderer='character_sheet.html') @view_config(route_name='sheet', renderer='character_sheet.html')
def sheet(request): def sheet(request):
return response_from(request.context) return response_from(request.context)
@view_config(route_name='data', renderer='json')
def data(request):
return response_from(request.context)