diff --git a/src/ttfrog/assets/templates/character_sheet.html b/src/ttfrog/assets/templates/character_sheet.html
index 1b39856..681b23c 100644
--- a/src/ttfrog/assets/templates/character_sheet.html
+++ b/src/ttfrog/assets/templates/character_sheet.html
@@ -16,7 +16,7 @@
{{ field('name') }}
{{ field('ancestry_id') }}
- {% for obj in c.form['classes'] %}
+ {% for obj in c.form['class_list'] %}
{{ obj(class='multiclass') }}
{% endfor %}
Add Class: {{ c.form['newclass'](class='multiclass') }}
@@ -143,8 +143,8 @@
Attributes
- {% if c.record.class_attributes %}
- {{ field('class_attributes') }}
+ {% if c.record.attribute_list %}
+ {{ field('attribute_list') }}
{% endif %}
@@ -173,7 +173,7 @@
{{ field }}: {{ msg }}
{% endfor %}
-{{ c.record.class_attributes }}
+{{ c.record }}
{% endblock %}
diff --git a/src/ttfrog/db/schema/character.py b/src/ttfrog/db/schema/character.py
index e3f0086..95dd675 100644
--- a/src/ttfrog/db/schema/character.py
+++ b/src/ttfrog/db/schema/character.py
@@ -67,8 +67,8 @@ class AncestryTrait(BaseObject, IterableMixin):
class CharacterClassMap(BaseObject, IterableMixin):
__tablename__ = "class_map"
id = Column(Integer, primary_key=True, autoincrement=True)
- character_id = Column(Integer, ForeignKey("character.id"))
- character_class_id = Column(Integer, ForeignKey("character_class.id"))
+ character_id = Column(Integer, ForeignKey("character.id"), nullable=False)
+ character_class_id = Column(Integer, ForeignKey("character_class.id"), nullable=False)
mapping = UniqueConstraint(character_id, character_class_id)
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}, default=1)
@@ -76,7 +76,7 @@ class CharacterClassMap(BaseObject, IterableMixin):
character = relationship("Character", uselist=False, viewonly=True)
def __repr__(self):
- return f"{self.character.name}, {self.character_class.name}, level {self.level}"
+ return "{self.character.name}, {self.character_class.name}, level {self.level}"
class CharacterClassAttributeMap(BaseObject, IterableMixin):
@@ -118,10 +118,10 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin):
proficiencies = Column(String)
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan")
- _classes = association_proxy("class_map", "id", creator=class_map_creator)
+ class_list = association_proxy("class_map", "id", creator=class_map_creator)
character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade="all,delete,delete-orphan")
- _class_attributes = association_proxy("character_class_attribute_map", "id", creator=attr_map_creator)
+ attribute_list = association_proxy("character_class_attribute_map", "id", creator=attr_map_creator)
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False, default="1")
ancestry = relationship("Ancestry", uselist=False)
@@ -153,8 +153,13 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin):
if level_in_class:
level_in_class = level_in_class[0]
level_in_class.level = level
- return
- self._classes.append(CharacterClassMap(character_id=self.id, character_class_id=newclass.id, level=level))
+ else:
+ self.class_list.append(CharacterClassMap(character_id=self.id, character_class_id=newclass.id, level=level))
+ for lvl in range(1, level + 1):
+ if not newclass.attributes_by_level[lvl]:
+ continue
+ for attr_name, attr in newclass.attributes_by_level[lvl].items():
+ self.add_class_attribute(attr, attr.options[0])
def remove_class(self, target):
self.class_map = [m for m in self.class_map if m.id != target.id]
@@ -167,8 +172,9 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin):
def add_class_attribute(self, attribute, option):
for thisclass in self.classes.values():
+ # this test is failing?
if attribute.name in thisclass.attributes_by_level.get(self.levels[thisclass.name], {}):
- self._class_attributes.append(
+ self.attribute_list.append(
CharacterClassAttributeMap(
character_id=self.id, class_attribute_id=attribute.id, option_id=option.id
)
diff --git a/src/ttfrog/db/schema/classes.py b/src/ttfrog/db/schema/classes.py
index 0f20482..2f81354 100644
--- a/src/ttfrog/db/schema/classes.py
+++ b/src/ttfrog/db/schema/classes.py
@@ -1,3 +1,5 @@
+from collections import defaultdict
+
from sqlalchemy import Column, Enum, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
@@ -47,7 +49,7 @@ class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
@property
def attributes_by_level(self):
- by_level = {}
+ by_level = defaultdict(list)
for mapping in self.attributes:
by_level[mapping.level] = {mapping.attribute.name: mapping.attribute}
return by_level
diff --git a/src/ttfrog/webserver/controllers/character_sheet.py b/src/ttfrog/webserver/controllers/character_sheet.py
index 9d9e0ee..b592b77 100644
--- a/src/ttfrog/webserver/controllers/character_sheet.py
+++ b/src/ttfrog/webserver/controllers/character_sheet.py
@@ -45,7 +45,8 @@ class ClassAttributesFormField(FormField):
def process(self, *args, **kwargs):
super().process(*args, **kwargs)
self.character_class_map = db.query(CharacterClassAttributeMap).get(self.data["id"])
- self.label.text = self.character_class_map.character_class[0].name
+ if self.character_class_map:
+ self.label.text = self.character_class_map.character_class.name
class ClassAttributesForm(ModelForm):
@@ -56,6 +57,7 @@ class ClassAttributesForm(ModelForm):
def __init__(self, formdata=None, obj=None, prefix=None):
if obj:
+ logging.debug(f"Loading existing attribute {self = } {formdata = } {obj = }")
obj = db.query(CharacterClassAttributeMap).get(obj)
super().__init__(formdata=formdata, obj=obj, prefix=prefix)
@@ -77,6 +79,7 @@ class MulticlassForm(ModelForm):
to an instance. This will ensure that the rendered field is populated with the current
value of the class_map.
"""
+ logging.debug(f"Loading existing class {self = } {formdata = } {obj = }")
if obj:
obj = db.query(CharacterClassMap).get(obj)
super().__init__(formdata=formdata, obj=obj, prefix=prefix)
@@ -90,10 +93,10 @@ class CharacterForm(ModelForm):
save = SubmitField()
delete = SubmitField()
ancestry_id = DeferredSelectField("Ancestry", model=Ancestry, default=1, validate_choice=True, widget=Select())
- classes = FieldList(FormField(MulticlassForm, label=None, widget=ListWidget()), min_entries=0)
+ class_list = FieldList(FormField(MulticlassForm, label=None, widget=ListWidget()), min_entries=0)
newclass = FormField(MulticlassForm, widget=ListWidget())
- class_attributes = FieldList(
+ attribute_list = FieldList(
ClassAttributesFormField(ClassAttributesForm, widget=ClassAttributeWidget()), min_entries=1
)
@@ -115,12 +118,12 @@ class CharacterSheet(BaseController):
Validate multiclass fields in form data.
"""
ret = super().validate()
- if not self.form.data["classes"]:
+ if not self.form.data["class_list"]:
return ret
err = ""
total_level = 0
- for field in self.form.data["classes"]:
+ for field in self.form.data["class_list"]:
level = field.get("level")
total_level += level
if level not in VALID_LEVELS:
@@ -134,40 +137,16 @@ class CharacterSheet(BaseController):
return ret and True
def add_class_attributes(self):
- # prefetch the records for each of the character's classes
- classes_by_id = {
- c.id: c
- for c in db.query(CharacterClass)
- .filter(CharacterClass.id.in_(c.character_class_id for c in self.record.class_map))
- .all()
- }
-
- assigned = [int(m.class_attribute_id) for m in self.record.character_class_attribute_map]
- logging.debug(f"{assigned = }")
-
# step through the list of class mappings for this character
- for class_map in self.record.class_map:
- thisclass = classes_by_id[class_map.character_class_id]
-
- # assign each class attribute available at the character's current
- # level to the list of the character's class attributes
- for attr_map in [a for a in thisclass.attributes if a.level <= class_map.level]:
- # when creating a record, assign the first of the available
- # options to the character's class attribute.
- default_option = (
- db.query(ClassAttributeOption).filter_by(attribute_id=attr_map.class_attribute_id).first()
- )
-
- if attr_map.class_attribute_id not in assigned:
- self.record.class_attributes.append(
- {
- "class_attribute_id": attr_map.class_attribute_id,
- "option_id": default_option.id,
- }
- )
+ for class_name, class_def in self.record.classes.items():
+ logging.error(f"{class_name = }, {class_def = }")
+ for level in range(1, self.record.levels[class_name] + 1):
+ for attr in class_def.attributes_by_level.get(level, None):
+ self.record.add_class_attribute(attr, attr.options[0])
def save_callback(self):
- self.add_class_attributes()
+ # self.add_class_attributes()
+ pass
def populate(self):
"""
@@ -176,16 +155,16 @@ class CharacterSheet(BaseController):
"""
# multiclass form
- classes_formdata = self.form.data["classes"]
+ classes_formdata = self.form.data["class_list"]
classes_formdata.append(self.form.data["newclass"])
- del self.form.classes
+ del self.form.class_list
del self.form.newclass
# class attributes
- attrs_formdata = self.form.data["class_attributes"]
- del self.form.class_attributes
+ attrs_formdata = self.form.data["attribute_list"]
+ del self.form.attribute_list
super().populate()
- self.record.classes = self.populate_association("character_class_id", classes_formdata)
- self.record.class_attributes = self.populate_association("class_attribute_id", attrs_formdata)
+ self.record.class_list = self.populate_association("character_class_id", classes_formdata)
+ self.record.attribute_list = self.populate_association("class_attribute_id", attrs_formdata)
diff --git a/test/conftest.py b/test/conftest.py
index 508e9fb..5352647 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -30,12 +30,16 @@ def db(monkeypatch):
@pytest.fixture
-def classes(db):
+def classes_factory(db):
load_fixture(db, "classes")
- return dict((rec.name, rec) for rec in db.session.query(schema.CharacterClass).all())
+ def factory():
+ return dict((rec.name, rec) for rec in db.session.query(schema.CharacterClass).all())
+ return factory
@pytest.fixture
-def ancestries(db):
+def ancestries_factory(db):
load_fixture(db, "ancestry")
- return dict((rec.name, rec) for rec in db.session.query(schema.Ancestry).all())
+ def factory():
+ return dict((rec.name, rec) for rec in db.session.query(schema.Ancestry).all())
+ return factory
diff --git a/test/test_schema.py b/test/test_schema.py
index bbe5df5..b57cfb9 100644
--- a/test/test_schema.py
+++ b/test/test_schema.py
@@ -1,8 +1,11 @@
from ttfrog.db import schema
-def test_create_character(db, classes, ancestries):
+def test_create_character(db, classes_factory, ancestries_factory):
with db.transaction():
+ # load the fixtures so they are bound to the current session
+ classes = classes_factory()
+ ancestries = ancestries_factory()
darkvision = db.session.query(schema.AncestryTrait).filter_by(name="Darkvision")[0]
# create a human character (the default)