121 lines
4.4 KiB
Python
121 lines
4.4 KiB
Python
import logging
|
|
|
|
from ttfrog.webserver.controllers.base import BaseController
|
|
from ttfrog.webserver.forms import DeferredSelectField
|
|
from ttfrog.webserver.forms import NullableDeferredSelectField
|
|
from ttfrog.db.schema import Character, Ancestry, CharacterClass, CharacterClassMap
|
|
from ttfrog.db.base import STATS
|
|
from ttfrog.db.manager import db
|
|
|
|
from wtforms_alchemy import ModelForm
|
|
from wtforms.fields import SubmitField, SelectField, SelectMultipleField, FieldList, FormField, HiddenField
|
|
from wtforms.widgets import Select, ListWidget
|
|
from wtforms import ValidationError
|
|
|
|
VALID_LEVELS = range(1, 21)
|
|
|
|
|
|
class MulticlassForm(ModelForm):
|
|
id = HiddenField()
|
|
character_class_id = NullableDeferredSelectField(
|
|
'CharacterClass',
|
|
model=CharacterClass,
|
|
validate_choice=True,
|
|
widget=Select(),
|
|
coerce=int
|
|
)
|
|
level = SelectField(choices=VALID_LEVELS, default=1, coerce=int, validate_choice=True, widget=Select())
|
|
|
|
def __init__(self, formdata=None, obj=None, prefix=None):
|
|
"""
|
|
Populate the form field with a CharacterClassMap object by converting the object ID
|
|
to an instance. This will ensure that the rendered field is populated with the current
|
|
value of the class_map.
|
|
"""
|
|
obj = db.query(CharacterClassMap).get(obj)
|
|
super().__init__(formdata=formdata, obj=obj, prefix=prefix)
|
|
|
|
|
|
class CharacterForm(ModelForm):
|
|
class Meta:
|
|
model = Character
|
|
exclude = ['slug']
|
|
|
|
save = SubmitField()
|
|
delete = SubmitField()
|
|
ancestry_id = DeferredSelectField('Ancestry', model=Ancestry, default=1, validate_choice=True, widget=Select())
|
|
classes = FieldList(FormField(MulticlassForm, widget=ListWidget()), min_entries=0)
|
|
newclass = FormField(MulticlassForm, widget=ListWidget())
|
|
saving_throws = SelectMultipleField('Saving Throws', validate_choice=True, choices=STATS)
|
|
|
|
|
|
class CharacterSheet(BaseController):
|
|
model = CharacterForm.Meta.model
|
|
model_form = CharacterForm
|
|
|
|
@property
|
|
def resources(self):
|
|
return super().resources + [
|
|
{'type': 'script', 'uri': 'js/character_sheet.js'},
|
|
]
|
|
|
|
def populate_class_map(self, formdata):
|
|
"""
|
|
Populate the record's class_map association_proxy with dictionaries of
|
|
CharacterClassMap field data. The creator factory on the proxy will
|
|
convert dictionary data to CharacterClassMap instances..
|
|
"""
|
|
populated = []
|
|
for field in formdata:
|
|
class_map_id = field.pop('id')
|
|
class_map_id = int(class_map_id) if class_map_id else 0
|
|
logging.debug(f"{class_map_id = }, {field = }, {self.record.classes = }")
|
|
if not field['character_class_id']:
|
|
continue
|
|
elif not class_map_id:
|
|
populated.append(field)
|
|
else:
|
|
field['id'] = class_map_id
|
|
populated.append(field)
|
|
self.record.classes = populated
|
|
|
|
def validate_multiclass_form(self):
|
|
"""
|
|
Validate multiclass fields in form data.
|
|
"""
|
|
err = ""
|
|
total_level = 0
|
|
for field in self.form.data['classes']:
|
|
level = field.get('level', 0)
|
|
total_level += level
|
|
if level not in VALID_LEVELS:
|
|
err = f"Multiclass form field {field = } level is outside possible range."
|
|
break
|
|
if self.record.id and field.get('character_id', None) != self.record.id:
|
|
err = f"Multiclass form field {field = } does not match character ID {self.record.id}"
|
|
break
|
|
if total_level not in VALID_LEVELS:
|
|
err = f"Total level for all multiclasses ({total_level}) is outside possible range."
|
|
if err:
|
|
logging.error(err)
|
|
raise ValidationError(err)
|
|
|
|
def validate(self):
|
|
"""
|
|
Add custom validation of the multiclass form data to standard form validation.
|
|
"""
|
|
super().validate()
|
|
self.validate_multiclass_form()
|
|
|
|
def populate(self):
|
|
"""
|
|
Delete the multiclass form data before calling form.populate_obj() and use
|
|
our own method for populating the fieldlist.
|
|
"""
|
|
formdata = self.form.data['classes']
|
|
formdata.append(self.form.data['newclass'])
|
|
del self.form.classes
|
|
del self.form.newclass
|
|
super().populate()
|
|
self.populate_class_map(formdata)
|