This commit is contained in:
evilchili 2024-04-14 11:37:34 -07:00
parent dbb9461b7a
commit 44cd8fe9c9
6 changed files with 54 additions and 60 deletions

View File

@ -16,7 +16,7 @@
<div> <div>
{{ field('name') }} {{ field('name') }}
{{ field('ancestry_id') }} {{ field('ancestry_id') }}
{% for obj in c.form['classes'] %} {% for obj in c.form['class_list'] %}
{{ obj(class='multiclass') }} {{ obj(class='multiclass') }}
{% endfor %} {% endfor %}
<span class='label'>Add Class:</span> {{ c.form['newclass'](class='multiclass') }} <span class='label'>Add Class:</span> {{ c.form['newclass'](class='multiclass') }}
@ -143,8 +143,8 @@
</div> </div>
<div class='card'> <div class='card'>
<div class='label'>Attributes</div> <div class='label'>Attributes</div>
{% if c.record.class_attributes %} {% if c.record.attribute_list %}
{{ field('class_attributes') }} {{ field('attribute_list') }}
{% endif %} {% endif %}
</div> </div>
<div class='card'> <div class='card'>
@ -173,7 +173,7 @@
{{ field }}: {{ msg }} {{ field }}: {{ msg }}
{% endfor %} {% endfor %}
</code> </code>
{{ c.record.class_attributes }} {{ c.record }}
</code> </code>
{% endblock %} {% endblock %}

View File

@ -67,8 +67,8 @@ class AncestryTrait(BaseObject, IterableMixin):
class CharacterClassMap(BaseObject, IterableMixin): class CharacterClassMap(BaseObject, IterableMixin):
__tablename__ = "class_map" __tablename__ = "class_map"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
character_id = Column(Integer, ForeignKey("character.id")) character_id = Column(Integer, ForeignKey("character.id"), nullable=False)
character_class_id = Column(Integer, ForeignKey("character_class.id")) character_class_id = Column(Integer, ForeignKey("character_class.id"), nullable=False)
mapping = UniqueConstraint(character_id, character_class_id) mapping = UniqueConstraint(character_id, character_class_id)
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}, default=1) 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) character = relationship("Character", uselist=False, viewonly=True)
def __repr__(self): 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): class CharacterClassAttributeMap(BaseObject, IterableMixin):
@ -118,10 +118,10 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin):
proficiencies = Column(String) proficiencies = Column(String)
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan") 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") 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_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False, default="1")
ancestry = relationship("Ancestry", uselist=False) ancestry = relationship("Ancestry", uselist=False)
@ -153,8 +153,13 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin):
if level_in_class: if level_in_class:
level_in_class = level_in_class[0] level_in_class = level_in_class[0]
level_in_class.level = level level_in_class.level = level
return else:
self._classes.append(CharacterClassMap(character_id=self.id, character_class_id=newclass.id, level=level)) 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): def remove_class(self, target):
self.class_map = [m for m in self.class_map if m.id != target.id] 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): def add_class_attribute(self, attribute, option):
for thisclass in self.classes.values(): for thisclass in self.classes.values():
# this test is failing?
if attribute.name in thisclass.attributes_by_level.get(self.levels[thisclass.name], {}): if attribute.name in thisclass.attributes_by_level.get(self.levels[thisclass.name], {}):
self._class_attributes.append( self.attribute_list.append(
CharacterClassAttributeMap( CharacterClassAttributeMap(
character_id=self.id, class_attribute_id=attribute.id, option_id=option.id character_id=self.id, class_attribute_id=attribute.id, option_id=option.id
) )

View File

@ -1,3 +1,5 @@
from collections import defaultdict
from sqlalchemy import Column, Enum, ForeignKey, Integer, String from sqlalchemy import Column, Enum, ForeignKey, Integer, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@ -47,7 +49,7 @@ class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
@property @property
def attributes_by_level(self): def attributes_by_level(self):
by_level = {} by_level = defaultdict(list)
for mapping in self.attributes: for mapping in self.attributes:
by_level[mapping.level] = {mapping.attribute.name: mapping.attribute} by_level[mapping.level] = {mapping.attribute.name: mapping.attribute}
return by_level return by_level

View File

@ -45,7 +45,8 @@ class ClassAttributesFormField(FormField):
def process(self, *args, **kwargs): def process(self, *args, **kwargs):
super().process(*args, **kwargs) super().process(*args, **kwargs)
self.character_class_map = db.query(CharacterClassAttributeMap).get(self.data["id"]) 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): class ClassAttributesForm(ModelForm):
@ -56,6 +57,7 @@ class ClassAttributesForm(ModelForm):
def __init__(self, formdata=None, obj=None, prefix=None): def __init__(self, formdata=None, obj=None, prefix=None):
if obj: if obj:
logging.debug(f"Loading existing attribute {self = } {formdata = } {obj = }")
obj = db.query(CharacterClassAttributeMap).get(obj) obj = db.query(CharacterClassAttributeMap).get(obj)
super().__init__(formdata=formdata, obj=obj, prefix=prefix) 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 to an instance. This will ensure that the rendered field is populated with the current
value of the class_map. value of the class_map.
""" """
logging.debug(f"Loading existing class {self = } {formdata = } {obj = }")
if obj: if obj:
obj = db.query(CharacterClassMap).get(obj) obj = db.query(CharacterClassMap).get(obj)
super().__init__(formdata=formdata, obj=obj, prefix=prefix) super().__init__(formdata=formdata, obj=obj, prefix=prefix)
@ -90,10 +93,10 @@ class CharacterForm(ModelForm):
save = SubmitField() save = SubmitField()
delete = SubmitField() delete = SubmitField()
ancestry_id = DeferredSelectField("Ancestry", model=Ancestry, default=1, validate_choice=True, widget=Select()) 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()) newclass = FormField(MulticlassForm, widget=ListWidget())
class_attributes = FieldList( attribute_list = FieldList(
ClassAttributesFormField(ClassAttributesForm, widget=ClassAttributeWidget()), min_entries=1 ClassAttributesFormField(ClassAttributesForm, widget=ClassAttributeWidget()), min_entries=1
) )
@ -115,12 +118,12 @@ class CharacterSheet(BaseController):
Validate multiclass fields in form data. Validate multiclass fields in form data.
""" """
ret = super().validate() ret = super().validate()
if not self.form.data["classes"]: if not self.form.data["class_list"]:
return ret return ret
err = "" err = ""
total_level = 0 total_level = 0
for field in self.form.data["classes"]: for field in self.form.data["class_list"]:
level = field.get("level") level = field.get("level")
total_level += level total_level += level
if level not in VALID_LEVELS: if level not in VALID_LEVELS:
@ -134,40 +137,16 @@ class CharacterSheet(BaseController):
return ret and True return ret and True
def add_class_attributes(self): 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 # step through the list of class mappings for this character
for class_map in self.record.class_map: for class_name, class_def in self.record.classes.items():
thisclass = classes_by_id[class_map.character_class_id] logging.error(f"{class_name = }, {class_def = }")
for level in range(1, self.record.levels[class_name] + 1):
# assign each class attribute available at the character's current for attr in class_def.attributes_by_level.get(level, None):
# level to the list of the character's class attributes self.record.add_class_attribute(attr, attr.options[0])
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,
}
)
def save_callback(self): def save_callback(self):
self.add_class_attributes() # self.add_class_attributes()
pass
def populate(self): def populate(self):
""" """
@ -176,16 +155,16 @@ class CharacterSheet(BaseController):
""" """
# multiclass form # multiclass form
classes_formdata = self.form.data["classes"] classes_formdata = self.form.data["class_list"]
classes_formdata.append(self.form.data["newclass"]) classes_formdata.append(self.form.data["newclass"])
del self.form.classes del self.form.class_list
del self.form.newclass del self.form.newclass
# class attributes # class attributes
attrs_formdata = self.form.data["class_attributes"] attrs_formdata = self.form.data["attribute_list"]
del self.form.class_attributes del self.form.attribute_list
super().populate() super().populate()
self.record.classes = self.populate_association("character_class_id", classes_formdata) self.record.class_list = self.populate_association("character_class_id", classes_formdata)
self.record.class_attributes = self.populate_association("class_attribute_id", attrs_formdata) self.record.attribute_list = self.populate_association("class_attribute_id", attrs_formdata)

View File

@ -30,12 +30,16 @@ def db(monkeypatch):
@pytest.fixture @pytest.fixture
def classes(db): def classes_factory(db):
load_fixture(db, "classes") load_fixture(db, "classes")
def factory():
return dict((rec.name, rec) for rec in db.session.query(schema.CharacterClass).all()) return dict((rec.name, rec) for rec in db.session.query(schema.CharacterClass).all())
return factory
@pytest.fixture @pytest.fixture
def ancestries(db): def ancestries_factory(db):
load_fixture(db, "ancestry") load_fixture(db, "ancestry")
def factory():
return dict((rec.name, rec) for rec in db.session.query(schema.Ancestry).all()) return dict((rec.name, rec) for rec in db.session.query(schema.Ancestry).all())
return factory

View File

@ -1,8 +1,11 @@
from ttfrog.db import schema from ttfrog.db import schema
def test_create_character(db, classes, ancestries): def test_create_character(db, classes_factory, ancestries_factory):
with db.transaction(): 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] darkvision = db.session.query(schema.AncestryTrait).filter_by(name="Darkvision")[0]
# create a human character (the default) # create a human character (the default)