restructuring for poetry-slam

This commit is contained in:
evilchili
2024-03-26 00:53:21 -07:00
parent b1d7639a62
commit 78115023bb
38 changed files with 483 additions and 465 deletions
View File
+26
View File
@@ -0,0 +1,26 @@
import logging
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from ttfrog.db.manager import db
from ttfrog.webserver.routes import routes
def configuration():
config = Configurator(settings={"sqlalchemy.url": db.url, "jinja2.directories": "ttfrog.assets:templates/"})
config.include("pyramid_tm")
config.include("pyramid_sqlalchemy")
config.include("pyramid_jinja2")
config.add_static_view(name="/static", path="ttfrog.assets:static/")
config.add_jinja2_renderer(".html", settings_prefix="jinja2.")
return config
def start(host: str, port: int, debug: bool = False) -> None:
logging.debug(f"Configuring webserver with {host=}, {port=}, {debug=}")
config = configuration()
config.include(routes)
config.scan("ttfrog.webserver.views")
make_server(host, int(port), config.make_wsgi_app()).serve_forever()
@@ -0,0 +1,5 @@
from .base import BaseController
from .character_sheet import CharacterSheet
from .json_data import JsonData
__all__ = [BaseController, CharacterSheet, JsonData]
@@ -0,0 +1,13 @@
from wtforms_alchemy import ModelForm
from ttfrog.db.manager import db
from ttfrog.db.schema import Ancestry
class AncestryForm(ModelForm):
class Meta:
model = Ancestry
exclude = ["slug"]
def get_session():
return db.session
+149
View File
@@ -0,0 +1,149 @@
import logging
import re
from collections import defaultdict
from pyramid.httpexceptions import HTTPFound
from pyramid.interfaces import IRoutesMapper
from ttfrog.db.manager import db
def get_all_routes(request):
routes = {
"static": "/static",
}
uri_pattern = re.compile(r"^([^\{\*]+)")
mapper = request.registry.queryUtility(IRoutesMapper)
for route in mapper.get_routes():
if route.name.startswith("__"):
continue
m = uri_pattern.search(route.pattern)
if m:
routes[route.name] = m.group(0)
return routes
class BaseController:
model = None
model_form = None
def __init__(self, request):
self.request = request
self.attrs = defaultdict(str)
self._slug = None
self._record = None
self._form = None
self.config = {"static_url": "/static", "project_name": "TTFROG"}
self.configure_for_model()
@property
def slug(self):
if not self._slug:
parts = self.request.matchdict.get("uri", "").split("-")
self._slug = parts[0].replace("/", "")
return self._slug
@property
def record(self):
if not self._record and self.model:
try:
self._record = db.query(self.model).filter(self.model.slug == self.slug)[0]
except IndexError:
logging.warning(f"Could not load record with slug {self.slug}")
self._record = self.model()
return self._record
@property
def form(self):
if not self.model:
return
if not self.model_form:
return
if not self._form:
if self.request.POST:
self._form = self.model_form(self.request.POST, obj=self.record)
else:
self._form = self.model_form(obj=self.record)
if not self.record.id:
# apply the db schema defaults
self._form.process()
return self._form
@property
def resources(self):
return [
{"type": "style", "uri": "css/styles.css"},
]
def configure_for_model(self):
if "all_records" not in self.attrs:
self.attrs["all_records"] = db.query(self.model).all()
def template_context(self, **kwargs) -> dict:
return dict(
config=self.config,
request=self.request,
form=self.form,
record=self.record,
routes=get_all_routes(self.request),
resources=self.resources,
**self.attrs,
**kwargs,
)
def populate(self):
self.form.populate_obj(self.record)
def populate_association(self, key, formdata):
populated = []
for field in formdata:
map_id = field.pop("id")
map_id = int(map_id) if map_id else 0
if not field[key]:
continue
elif not map_id:
populated.append(field)
else:
field["id"] = map_id
populated.append(field)
return populated
def validate(self):
return self.form.validate()
def save(self):
if not self.form.save.data:
return
if not self.validate():
return
logging.debug(f"{self.form.data = }")
# previous = dict(self.record)
logging.debug(f"{self.record = }")
self.populate()
# transaction_log.record(previous, self.record)
with db.transaction():
db.add(self.record)
self.save_callback()
logging.debug(f"Saved {self.record = }")
location = self.request.current_route_path()
if self.record.slug not in location:
location = f"{location}/{self.record.uri}"
logging.debug(f"Redirecting to {location}")
return HTTPFound(location=location)
def delete(self):
if not self.record.id:
return
with db.transaction():
db.query(self.model).filter_by(id=self.record.id).delete()
location = self.request.current_route_path()
return HTTPFound(location=location)
def response(self):
if not self.form:
return
elif self.form.save.data:
return self.save()
elif self.form.delete.data:
return self.delete()
@@ -0,0 +1,191 @@
import logging
from markupsafe import Markup
from wtforms import ValidationError
from wtforms.fields import FieldList, FormField, HiddenField, SelectField, SelectMultipleField, SubmitField
from wtforms.validators import Optional
from wtforms.widgets import ListWidget, Select
from wtforms.widgets.core import html_params
from wtforms_alchemy import ModelForm
from ttfrog.db.base import STATS
from ttfrog.db.manager import db
from ttfrog.db.schema import (
Ancestry,
Character,
CharacterClass,
CharacterClassAttributeMap,
CharacterClassMap,
ClassAttributeOption,
)
from ttfrog.webserver.controllers.base import BaseController
from ttfrog.webserver.forms import DeferredSelectField, NullableDeferredSelectField
VALID_LEVELS = range(1, 21)
class ClassAttributeWidget:
def __call__(self, field, **kwargs):
kwargs.setdefault("id", field.id)
html = [
f"<span {html_params(**kwargs)}>{field.character_class_map.class_attribute.name}</span>",
"<span>",
]
for subfield in field:
html.append(subfield())
html.append("</span>")
return Markup("".join(html))
class ClassAttributesFormField(FormField):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.character_class_map = None
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
class ClassAttributesForm(ModelForm):
id = HiddenField()
class_attribute_id = HiddenField()
option_id = SelectField(widget=Select(), choices=[], validators=[Optional()], coerce=int)
def __init__(self, formdata=None, obj=None, prefix=None):
if obj:
obj = db.query(CharacterClassAttributeMap).get(obj)
super().__init__(formdata=formdata, obj=obj, prefix=prefix)
if obj:
options = db.query(ClassAttributeOption).filter_by(attribute_id=obj.class_attribute.id)
self.option_id.choices = [(rec.id, rec.name) for rec in options.all()]
class MulticlassForm(ModelForm):
id = HiddenField()
character_class_id = NullableDeferredSelectField(
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.
"""
if obj:
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, label=None, widget=ListWidget()), min_entries=0)
newclass = FormField(MulticlassForm, widget=ListWidget())
class_attributes = FieldList(
ClassAttributesFormField(ClassAttributesForm, widget=ClassAttributeWidget()), min_entries=1
)
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 validate_callback(self):
"""
Validate multiclass fields in form data.
"""
ret = super().validate()
if not self.form.data["classes"]:
return ret
err = ""
total_level = 0
for field in self.form.data["classes"]:
level = field.get("level")
total_level += level
if level not in VALID_LEVELS:
err = f"Multiclass form field {field = } level is outside possible range."
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)
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,
}
)
def save_callback(self):
self.add_class_attributes()
def populate(self):
"""
Delete the association proxies' form data before calling form.populate_obj(),
and instead use our own methods for populating the fieldlist.
"""
# multiclass form
classes_formdata = self.form.data["classes"]
classes_formdata.append(self.form.data["newclass"])
del self.form.classes
del self.form.newclass
# class attributes
attrs_formdata = self.form.data["class_attributes"]
del self.form.class_attributes
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)
@@ -0,0 +1,21 @@
from pyramid.httpexceptions import exception_response
from ttfrog.db import schema
from ttfrog.db.manager import db
from .base import BaseController
class JsonData(BaseController):
model = None
model_form = None
def configure_for_model(self):
try:
self.model = getattr(schema, self.request.matchdict.get("table_name"))
except AttributeError:
raise exception_response(404)
def response(self):
query = db.query(self.model).filter_by(**self.request.params)
return {"table_name": self.model.__tablename__, "records": query.all()}
+21
View File
@@ -0,0 +1,21 @@
from wtforms.fields import SelectField, SelectMultipleField
from ttfrog.db.manager import db
class DeferredSelectMultipleField(SelectMultipleField):
def __init__(self, *args, model=None, **kwargs):
super().__init__(*args, **kwargs)
self.choices = [(rec.id, rec.name) for rec in db.query(model).all()]
class DeferredSelectField(SelectField):
def __init__(self, *args, model=None, **kwargs):
super().__init__(*args, **kwargs)
self.choices = [(rec.id, getattr(rec, "name", str(rec))) for rec in db.query(model).all()]
class NullableDeferredSelectField(DeferredSelectField):
def __init__(self, *args, model=None, label="---", **kwargs):
super().__init__(*args, model=model, **kwargs)
self.choices = [(0, label)] + self.choices
+4
View File
@@ -0,0 +1,4 @@
def routes(config):
config.add_route("index", "/")
config.add_route("sheet", "/c{uri:.*}", factory="ttfrog.webserver.controllers.CharacterSheet")
config.add_route("data", "/_/{table_name}{uri:.*}", factory="ttfrog.webserver.controllers.JsonData")
+26
View File
@@ -0,0 +1,26 @@
from pyramid.response import Response
from pyramid.view import view_config
from ttfrog.attribute_map import AttributeMap
from ttfrog.db.manager import db
from ttfrog.db.schema import Ancestry
def response_from(controller):
return controller.response() or AttributeMap.from_dict({"c": controller.template_context()})
@view_config(route_name="index")
def index(request):
ancestries = [a.name for a in db.session.query(Ancestry).all()]
return Response(",".join(ancestries))
@view_config(route_name="sheet", renderer="character_sheet.html")
def sheet(request):
return response_from(request.context)
@view_config(route_name="data", renderer="json")
def data(request):
return response_from(request.context)