restructuring for poetry-slam
This commit is contained in:
@@ -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
|
||||
@@ -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()}
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user