diff --git a/ttfrog/assets/templates/character_sheet.html b/ttfrog/assets/templates/character_sheet.html index da20632..5b68859 100644 --- a/ttfrog/assets/templates/character_sheet.html +++ b/ttfrog/assets/templates/character_sheet.html @@ -15,7 +15,12 @@
{{ DISABLED }}
-{{ c }}
+{{ c.record }}
{% endblock %}
@@ -170,7 +183,7 @@
console.log("{{ field }}: {{ msg }}");
{% endfor %}
const TRAITS = {
-{% for trait_desc, traits in c.traits.items() %}
+{% for trait_desc, traits in [] %}
'{{ trait_desc }}': [
{% for trait in traits %}
{
diff --git a/ttfrog/db/base.py b/ttfrog/db/base.py
index 55ad7b9..53b8f8b 100644
--- a/ttfrog/db/base.py
+++ b/ttfrog/db/base.py
@@ -1,3 +1,4 @@
+import enum
import logging
import nanoid
from nanoid_dictionary import human_alphabet
@@ -33,7 +34,11 @@ class IterableMixin:
yield attr, values[attr]
for relname in self.__mapper__.relationships.keys():
relvals = []
- for rel in self.__getattribute__(relname):
+ reliter = self.__getattribute__(relname)
+ if not reliter:
+ yield relname, relvals
+ continue
+ for rel in reliter:
try:
relvals.append({k: v for k, v in vars(rel).items() if not k.startswith('_')})
except TypeError:
@@ -49,6 +54,9 @@ class IterableMixin:
serialized[key] = value
return serialized
+ def __repr__(self):
+ return str(dict(self))
+
def multivalue_string_factory(name, column=Column(String), separator=';'):
"""
@@ -76,5 +84,22 @@ def multivalue_string_factory(name, column=Column(String), separator=';'):
})
+class EnumField(enum.Enum):
+ """
+ A serializable enum.
+ """
+ def __json__(self, request):
+ return self.value
+
+
+SavingThrowsMixin = multivalue_string_factory('saving_throws')
+SkillsMixin = multivalue_string_factory('skills')
+
+STATS = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
+CREATURE_TYPES = ['aberation', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'Giant',
+ 'humanoid', 'monstrosity', 'ooze', 'plant', 'undead']
+CreatureTypesEnum = EnumField("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
+StatsEnum = EnumField("StatsEnum", ((k, k) for k in STATS))
+
# class Table(*Bases):
Bases = [BaseObject, IterableMixin, SlugMixin]
diff --git a/ttfrog/db/bootstrap.py b/ttfrog/db/bootstrap.py
index 171471e..0d0191a 100644
--- a/ttfrog/db/bootstrap.py
+++ b/ttfrog/db/bootstrap.py
@@ -49,15 +49,40 @@ data = {
{'id': 1, 'name': 'human', 'creature_type': 'humanoid'},
{'id': 2, 'name': 'dragonborn', 'creature_type': 'humanoid'},
{'id': 3, 'name': 'tiefling', 'creature_type': 'humanoid'},
+ {'id': 4, 'name': 'elf', 'creature_type': 'humanoid'},
+ ],
+
+ 'AncestryTrait': [
+ { 'id': 1, 'name': '+1 to All Ability Scores', },
+ { 'id': 2, 'name': 'Breath Weapon', },
+ { 'id': 3, 'name': 'Darkvision', },
+ ],
+
+ 'AncestryTraitMap': [
+ { 'ancestry_id': 1, 'ancestry_trait_id': 1, 'level': 1}, # human +1 to scores
+ { 'ancestry_id': 2, 'ancestry_trait_id': 2, 'level': 1}, # dragonborn breath weapon
+ { 'ancestry_id': 3, 'ancestry_trait_id': 3, 'level': 1}, # tiefling darkvision
+ { 'ancestry_id': 2, 'ancestry_trait_id': 2, 'level': 1}, # elf darkvision
+ ],
+
+ 'CharacterClassMap': [
+ {
+ 'character_id': 1,
+ 'character_class_id': 1,
+ 'level': 2,
+ },
+ {
+ 'character_id': 1,
+ 'character_class_id': 2,
+ 'level': 3,
+ },
],
'Character': [
{
'id': 1,
'name': 'Sabetha',
- 'ancestry': 'human',
- 'character_class': ['fighter', 'rogue'],
- 'level': 1,
+ 'ancestry_id': 1,
'armor_class': 10,
'max_hit_points': 14,
'hit_points': 14,
@@ -76,29 +101,18 @@ data = {
],
'ClassAttribute': [
- {
- 'character_class_id': 1,
- 'name': 'Fighting Style',
- 'value': 'Archery',
- 'level': 1,
- },
+ {'id': 1, 'name': 'Fighting Style', 'value': 'Archery'},
],
- 'AncestryTrait': [
- {
- 'id': 1,
- 'ancestry_id': 1,
- 'name': '+1 to All Ability Scores',
- 'level': 1,
- },
- {
- 'id': 2,
- 'ancestry_id': 2,
- 'name': 'Breath Weapon',
- 'level': 1,
- },
+ 'ClassAttributeMap': [
+ {'class_attribute_id': 1, 'character_class_id': 1, 'level': 2}, # Fighter: Archery fighting style
],
+ 'CharacterClassAttributeMap': [
+ {'class_attribute_id': 1, 'character_id': 1}, # Sabetha: Archery fighting style
+ ],
+
+
'Modifier': [
# Humans
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'str'},
diff --git a/ttfrog/db/schema.py b/ttfrog/db/schema.py
deleted file mode 100644
index 20cc847..0000000
--- a/ttfrog/db/schema.py
+++ /dev/null
@@ -1,141 +0,0 @@
-import enum
-
-from sqlalchemy import Column
-from sqlalchemy import Integer
-from sqlalchemy import String
-from sqlalchemy import ForeignKey
-from sqlalchemy import Enum
-from sqlalchemy import Text
-from sqlalchemy import UniqueConstraint
-from sqlalchemy.orm import relationship
-
-from ttfrog.db.base import Bases, BaseObject, IterableMixin
-from ttfrog.db.base import multivalue_string_factory
-
-
-STATS = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
-
-CREATURE_TYPES = ['aberation', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'Giant',
- 'humanoid', 'monstrosity', 'ooze', 'plant', 'undead']
-
-
-class EnumField(enum.Enum):
- """
- A serializable enum.
- """
- def __json__(self, request):
- return self.value
-
-
-# enums for db schemas
-StatsEnum = EnumField("StatsEnum", ((k, k) for k in STATS))
-CreatureTypesEnum = EnumField("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
-
-CharacterClassMixin = multivalue_string_factory('character_class', Column(String, nullable=False))
-SavingThrowsMixin = multivalue_string_factory('saving_throws')
-SkillsMixin = multivalue_string_factory('skills')
-
-
-class Skill(*Bases):
- __tablename__ = "skill"
- id = Column(Integer, primary_key=True, autoincrement=True)
- name = Column(String, index=True, unique=True)
- description = Column(Text)
-
- def __repr__(self):
- return str(self.name)
-
-
-class Proficiency(*Bases):
- __tablename__ = "proficiency"
- id = Column(Integer, primary_key=True, autoincrement=True)
- name = Column(String, index=True, unique=True)
-
- def __repr__(self):
- return str(self.name)
-
-
-class Ancestry(*Bases):
- __tablename__ = "ancestry"
- id = Column(Integer, primary_key=True, autoincrement=True)
- name = Column(String, index=True, unique=True)
- creature_type = Column(Enum(CreatureTypesEnum))
- traits = relationship("AncestryTrait")
-
- def __repr__(self):
- return str(self.name)
-
-
-class AncestryTrait(BaseObject, IterableMixin):
- __tablename__ = "ancestry_trait"
- id = Column(Integer, primary_key=True, autoincrement=True)
- ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False)
- name = Column(String, nullable=False)
- description = Column(Text)
- level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
-
-
-class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
- __tablename__ = "character_class"
- id = Column(Integer, primary_key=True, autoincrement=True)
- name = Column(String, index=True, unique=True)
- hit_dice = Column(String, default='1d6')
- hit_dice_stat = Column(Enum(StatsEnum))
- proficiencies = Column(String)
-
- def __repr__(self):
- return str(self.name)
-
-
-class ClassAttribute(BaseObject, IterableMixin):
- __tablename__ = "class_attribute"
- id = Column(Integer, primary_key=True, autoincrement=True)
- character_class_id = Column(Integer, ForeignKey("character_class.id"), nullable=False)
- name = Column(String, nullable=False)
- value = Column(String, nullable=False)
- description = Column(Text)
- level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
-
- def __repr__(self):
- return str(self.name)
-
-
-class Character(*Bases, CharacterClassMixin, SavingThrowsMixin, SkillsMixin):
- __tablename__ = "character"
- id = Column(Integer, primary_key=True, autoincrement=True)
- ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False, default='human')
- name = Column(String, default='New Character', nullable=False)
- 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})
- 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, 'max': 999})
- speed = Column(Integer, nullable=False, default=30, info={'min': 0, 'max': 99})
- str = 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})
- int = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
- wis = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
- cha = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
- proficiencies = Column(String)
-
-
-class Modifier(BaseObject, IterableMixin):
- __tablename__ = "modifier"
- __table_args__ = (
- UniqueConstraint('source_table_name', 'source_table_id', 'value', 'type', 'target'),
- )
- id = Column(Integer, primary_key=True, autoincrement=True)
- source_table_name = Column(String, index=True, nullable=False)
- source_table_id = Column(Integer, index=True, nullable=False)
- value = Column(String, nullable=False)
- type = Column(String, nullable=False)
- target = Column(String, nullable=False)
-
-
-class TransactionLog(BaseObject, IterableMixin):
- __tablename__ = "transaction_log"
- id = Column(Integer, primary_key=True, autoincrement=True)
- source_table_name = Column(String, index=True, nullable=False)
- primary_key = Column(Integer, index=True)
- diff = Column(Text)
diff --git a/ttfrog/db/schema/__init__.py b/ttfrog/db/schema/__init__.py
new file mode 100644
index 0000000..24db301
--- /dev/null
+++ b/ttfrog/db/schema/__init__.py
@@ -0,0 +1,4 @@
+from .character import *
+from .classes import *
+from .property import *
+from .transaction import *
diff --git a/ttfrog/db/schema/character.py b/ttfrog/db/schema/character.py
new file mode 100644
index 0000000..05a2db5
--- /dev/null
+++ b/ttfrog/db/schema/character.py
@@ -0,0 +1,91 @@
+from ttfrog.db.base import Bases, BaseObject, IterableMixin, SavingThrowsMixin, SkillsMixin
+from ttfrog.db.base import CreatureTypesEnum
+
+from sqlalchemy import Column
+from sqlalchemy import Enum
+from sqlalchemy import Integer
+from sqlalchemy import String
+from sqlalchemy import Text
+from sqlalchemy import ForeignKey
+from sqlalchemy.orm import relationship
+
+
+__all__ = [
+ 'Ancestry',
+ 'AncestryTrait',
+ 'AncestryTraitMap',
+ 'CharacterClassMap',
+ 'CharacterClassAttributeMap',
+ 'Character',
+]
+
+
+class AncestryTraitMap(BaseObject):
+ __tablename__ = "trait_map"
+ ancestry_id = Column(Integer, ForeignKey("ancestry.id"), primary_key=True)
+ ancestry_trait_id = Column(Integer, ForeignKey("ancestry_trait.id"), primary_key=True)
+ trait = relationship("AncestryTrait", lazy='immediate')
+ level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
+
+
+class Ancestry(*Bases):
+ """
+ A character ancestry ("race"), which has zero or more AncestryTraits.
+ """
+ __tablename__ = "ancestry"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ name = Column(String, index=True, unique=True)
+ creature_type = Column(Enum(CreatureTypesEnum))
+ traits = relationship("AncestryTraitMap", lazy='immediate')
+
+ def __repr__(self):
+ return self.name
+
+
+class AncestryTrait(BaseObject, IterableMixin):
+ """
+ A trait granted to a character via its Ancestry.
+ """
+ __tablename__ = "ancestry_trait"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ name = Column(String, nullable=False)
+ description = Column(Text)
+
+
+class CharacterClassMap(BaseObject):
+ __tablename__ = "class_map"
+ character_id = Column(Integer, ForeignKey("character.id"), primary_key=True)
+ character_class_id = Column(Integer, ForeignKey("character_class.id"), primary_key=True)
+ character_class = relationship("CharacterClass", lazy='immediate')
+ level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}, default=1)
+
+
+class CharacterClassAttributeMap(BaseObject):
+ __tablename__ = "character_class_attribute_map"
+ class_attribute_id = Column(Integer, ForeignKey("class_attribute.id"), primary_key=True)
+ character_id = Column(Integer, ForeignKey("character.id"), primary_key=True)
+ attribute = relationship("ClassAttribute", lazy='immediate')
+
+
+class Character(*Bases, SavingThrowsMixin, SkillsMixin):
+ __tablename__ = "character"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ name = Column(String, default='New Character', nullable=False)
+ 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})
+ 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, 'max': 999})
+ speed = Column(Integer, nullable=False, default=30, info={'min': 0, 'max': 99})
+ str = 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})
+ int = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
+ wis = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
+ cha = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
+ proficiencies = Column(String)
+
+ classes = relationship("CharacterClassMap")
+ attributes = relationship("CharacterClassAttributeMap")
+
+ ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False, default='1')
+ ancestry = relationship("Ancestry", uselist=False)
diff --git a/ttfrog/db/schema/classes.py b/ttfrog/db/schema/classes.py
new file mode 100644
index 0000000..6b0f257
--- /dev/null
+++ b/ttfrog/db/schema/classes.py
@@ -0,0 +1,43 @@
+from ttfrog.db.base import Bases, BaseObject, IterableMixin, SavingThrowsMixin, SkillsMixin
+from ttfrog.db.base import StatsEnum
+
+from sqlalchemy import Column
+from sqlalchemy import Enum
+from sqlalchemy import Integer
+from sqlalchemy import String
+from sqlalchemy import Text
+from sqlalchemy import ForeignKey
+from sqlalchemy.orm import relationship
+
+
+__all__ = [
+ 'ClassAttributeMap',
+ 'ClassAttribute',
+ 'CharacterClass',
+]
+
+
+class ClassAttributeMap(BaseObject):
+ __tablename__ = "class_attribute_map"
+ class_attribute_id = Column(Integer, ForeignKey("class_attribute.id"), primary_key=True)
+ character_class_id = Column(Integer, ForeignKey("character_class.id"), primary_key=True)
+ attribute = relationship("ClassAttribute", lazy='immediate')
+ level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}, default=1)
+
+
+class ClassAttribute(BaseObject, IterableMixin):
+ __tablename__ = "class_attribute"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ name = Column(String, nullable=False)
+ value = Column(String, nullable=False)
+ description = Column(Text)
+
+
+class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
+ __tablename__ = "character_class"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ name = Column(String, index=True, unique=True)
+ hit_dice = Column(String, default='1d6')
+ hit_dice_stat = Column(Enum(StatsEnum))
+ proficiencies = Column(String)
+ attributes = relationship("ClassAttributeMap")
diff --git a/ttfrog/db/schema/property.py b/ttfrog/db/schema/property.py
new file mode 100644
index 0000000..3899857
--- /dev/null
+++ b/ttfrog/db/schema/property.py
@@ -0,0 +1,46 @@
+from ttfrog.db.base import Bases, BaseObject, IterableMixin
+
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy import String
+from sqlalchemy import Text
+from sqlalchemy import UniqueConstraint
+
+
+__all__ = [
+ 'Skill',
+ 'Proficiency',
+ 'Modifier',
+]
+
+
+class Skill(*Bases):
+ __tablename__ = "skill"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ name = Column(String, index=True, unique=True)
+ description = Column(Text)
+
+ def __repr__(self):
+ return str(self.name)
+
+
+class Proficiency(*Bases):
+ __tablename__ = "proficiency"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ name = Column(String, index=True, unique=True)
+
+ def __repr__(self):
+ return str(self.name)
+
+
+class Modifier(BaseObject, IterableMixin):
+ __tablename__ = "modifier"
+ __table_args__ = (
+ UniqueConstraint('source_table_name', 'source_table_id', 'value', 'type', 'target'),
+ )
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ source_table_name = Column(String, index=True, nullable=False)
+ source_table_id = Column(Integer, index=True, nullable=False)
+ value = Column(String, nullable=False)
+ type = Column(String, nullable=False)
+ target = Column(String, nullable=False)
diff --git a/ttfrog/db/schema/transaction.py b/ttfrog/db/schema/transaction.py
new file mode 100644
index 0000000..ba9d929
--- /dev/null
+++ b/ttfrog/db/schema/transaction.py
@@ -0,0 +1,14 @@
+from ttfrog.db.base import BaseObject, IterableMixin
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy import String
+from sqlalchemy import Text
+
+__all__ = ['TransactionLog']
+
+class TransactionLog(BaseObject, IterableMixin):
+ __tablename__ = "transaction_log"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ source_table_name = Column(String, index=True, nullable=False)
+ primary_key = Column(Integer, index=True)
+ diff = Column(Text)
diff --git a/ttfrog/webserver/controllers/character_sheet.py b/ttfrog/webserver/controllers/character_sheet.py
index 143d455..c2939df 100644
--- a/ttfrog/webserver/controllers/character_sheet.py
+++ b/ttfrog/webserver/controllers/character_sheet.py
@@ -1,8 +1,7 @@
from ttfrog.webserver.controllers.base import BaseController
from ttfrog.webserver.forms import DeferredSelectField, DeferredSelectMultipleField
-from ttfrog.db.schema import Character, Ancestry, CharacterClass, AncestryTrait, Modifier, STATS
-from ttfrog.db.manager import db
-from ttfrog.attribute_map import AttributeMap
+from ttfrog.db.schema import Character, Ancestry, CharacterClass
+from ttfrog.db.base import STATS
from wtforms_alchemy import ModelForm
from wtforms.fields import SubmitField, SelectMultipleField
@@ -17,7 +16,7 @@ class CharacterForm(ModelForm):
save = SubmitField()
delete = SubmitField()
- ancestry = DeferredSelectField('Ancestry', model=Ancestry, default='human', validate_choice=True, widget=Select())
+ ancestry = DeferredSelectField('Ancestry', model=Ancestry, default=1, validate_choice=True, widget=Select())
character_class = DeferredSelectMultipleField(
'CharacterClass',
@@ -38,14 +37,3 @@ class CharacterSheet(BaseController):
return super().resources + [
{'type': 'script', 'uri': 'js/character_sheet.js'},
]
-
- def template_context(self, **kwargs) -> dict:
- ctx = super().template_context(**kwargs)
- if self.record.ancestry:
- ancestry = db.query(Ancestry).filter_by(name=self.record.ancestry).one()
- ctx['traits'] = {}
- for trait in db.query(AncestryTrait).filter_by(ancestry_id=ancestry.id).all():
- ctx['traits'][trait.description] = db.query(Modifier).filter_by(source_table_name=trait.__tablename__, source_table_id=trait.id).all()
- else:
- ctx['traits'] = {};
- return ctx
diff --git a/ttfrog/webserver/controllers/json_data.py b/ttfrog/webserver/controllers/json_data.py
new file mode 100644
index 0000000..44dc05e
--- /dev/null
+++ b/ttfrog/webserver/controllers/json_data.py
@@ -0,0 +1,25 @@
+import logging
+
+from ttfrog.db import schema
+from ttfrog.db.manager import db
+from .base import BaseController
+
+from pyramid.httpexceptions import exception_response
+
+
+class JsonData(BaseController):
+ model = None
+ model_form = None
+
+ def configure_for_model(self):
+ try:
+ self.model = getattr(schema, self.request.matchdict.get('table_name'))
+ except AttributeError:
+ raise exception_response(404)
+
+ def response(self):
+ query = db.query(self.model).filter_by(**self.request.params)
+ return {
+ 'table_name': self.model.__tablename__,
+ 'records': query.all()
+ }
diff --git a/ttfrog/webserver/forms.py b/ttfrog/webserver/forms.py
index b817b79..8cc738e 100644
--- a/ttfrog/webserver/forms.py
+++ b/ttfrog/webserver/forms.py
@@ -4,9 +4,10 @@ from wtforms.fields import SelectField, SelectMultipleField
class DeferredSelectMultipleField(SelectMultipleField):
def __init__(self, *args, model=None, **kwargs):
super().__init__(*args, **kwargs)
- self.choices = db.query(model).all()
+ self.choices = [(rec.id, rec.name) for rec in db.query(model).all()]
+
class DeferredSelectField(SelectField):
def __init__(self, *args, model=None, **kwargs):
super().__init__(*args, **kwargs)
- self.choices = db.query(model).all()
+ self.choices = [(rec.id, rec.name) for rec in db.query(model).all()]