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

View File

@ -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
)

View File

@ -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

View File

@ -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)

View File

@ -30,12 +30,16 @@ def db(monkeypatch):
@pytest.fixture
def classes(db):
def classes_factory(db):
load_fixture(db, "classes")
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")
def factory():
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
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)