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)