diff --git a/pyproject.toml b/pyproject.toml index c910ebe..cbd385b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,9 @@ pyramid-jinja2 = "^2.10" pyramid-sqlalchemy = "^1.6" wtforms-sqlalchemy = "^0.4.1" transaction = "^4.0" +unicode-slugify = "^0.1.5" +nanoid = "^2.0.0" +nanoid-dictionary = "^2.4.0" [build-system] diff --git a/ttfrog/assets/templates/base.html b/ttfrog/assets/templates/base.html index b8e3bad..4bd1add 100644 --- a/ttfrog/assets/templates/base.html +++ b/ttfrog/assets/templates/base.html @@ -1,9 +1,9 @@ - {{ config['project_name'] }}{% block title %}{% endblock %} - - + {{ c['config']['project_name'] }}{% block title %}{% endblock %} + + {% block headers %}{% endblock %} diff --git a/ttfrog/assets/templates/character_sheet.html b/ttfrog/assets/templates/character_sheet.html index 9d94481..a1100d3 100644 --- a/ttfrog/assets/templates/character_sheet.html +++ b/ttfrog/assets/templates/character_sheet.html @@ -3,19 +3,19 @@ {% block content %} -{{ build_list(all_records) }} +{{ build_list(c) }}
-

{{ record.name }}

+

{{ c['record'].name }}

- {{ form.csrf_token }} + {{ c['form'].csrf_token }} - {% if 'process' in form.errors %} - Error: {{ form.errors['process'] |join(',') }} + {% if 'process' in c['form'].errors %} + Error: {{ c['form'].errors['process'] |join(',') }} {% endif %} @@ -27,9 +27,7 @@ {% block debug %}

Debug

-

Record

-
{{ record }}
-

Config

-
{{ config }}
-
+
+{{ c }}
+
{% endblock %} diff --git a/ttfrog/assets/templates/list.html b/ttfrog/assets/templates/list.html index e76464a..595d317 100644 --- a/ttfrog/assets/templates/list.html +++ b/ttfrog/assets/templates/list.html @@ -1,9 +1,9 @@ -{% macro build_list(records) %} +{% macro build_list(c) %}
diff --git a/ttfrog/db/base.py b/ttfrog/db/base.py index bd69e51..7a3ec75 100644 --- a/ttfrog/db/base.py +++ b/ttfrog/db/base.py @@ -1,5 +1,26 @@ +import nanoid +from nanoid_dictionary import human_alphabet + from pyramid_sqlalchemy import BaseObject from wtforms import validators +from slugify import slugify + +from sqlalchemy import Column +from sqlalchemy import String + +def genslug(): + return nanoid.generate(human_alphabet[2:], 5) + + +class SlugMixin: + slug = Column(String, index=True, unique=True, default=genslug) + + @property + def uri(self): + return '-'.join([ + self.slug, + slugify(self.name.title().replace(' ', ''), ok='', only_ascii=True, lower=False) + ]) class IterableMixin: @@ -50,4 +71,4 @@ class FormValidatorMixin: # class Table(*Bases): -Bases = [BaseObject, IterableMixin, FormValidatorMixin] +Bases = [BaseObject, IterableMixin, FormValidatorMixin, SlugMixin] diff --git a/ttfrog/db/bootstrap.py b/ttfrog/db/bootstrap.py index 2847cfe..25bd9d7 100644 --- a/ttfrog/db/bootstrap.py +++ b/ttfrog/db/bootstrap.py @@ -1,11 +1,9 @@ import logging -import transaction from ttfrog.db.manager import db from ttfrog.db import schema from sqlalchemy.exc import IntegrityError -from sqlalchemy.inspection import inspect # move this to json or whatever data = { @@ -33,7 +31,6 @@ def bootstrap(): try: with db.transaction(): db.session.add(obj) - obj.slug = db.slugify(rec) except IntegrityError as e: if 'UNIQUE constraint failed' in str(e): logging.info(f"Skipping existing {table} {obj}") diff --git a/ttfrog/db/manager.py b/ttfrog/db/manager.py index 0be22a2..6f18766 100644 --- a/ttfrog/db/manager.py +++ b/ttfrog/db/manager.py @@ -30,7 +30,7 @@ class SQLDatabaseManager: def engine(self): return create_engine(self.url) - @property + @cached_property def session(self): return Session diff --git a/ttfrog/db/schema.py b/ttfrog/db/schema.py index 0f5113a..8987fb7 100644 --- a/ttfrog/db/schema.py +++ b/ttfrog/db/schema.py @@ -2,7 +2,6 @@ from sqlalchemy import Column from sqlalchemy import Integer from sqlalchemy import String from sqlalchemy import ForeignKey -from sqlalchemy import CheckConstraint # from sqlalchemy import PrimaryKeyConstraint # from sqlalchemy import DateTime @@ -14,20 +13,18 @@ class Ancestry(*Bases): id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String, index=True, unique=True) - slug = Column(String, index=True, unique=True) class Character(*Bases): __tablename__ = "character" id = Column(Integer, primary_key=True, autoincrement=True) - slug = Column(String, index=True, unique=True) ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False) name = Column(String(255), nullable=False) level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}) - str = Column(Integer, info={'min': 1}) - dex = Column(Integer, info={'min': 1}) - con = Column(Integer, info={'min': 1}) - int = Column(Integer, info={'min': 1}) - wis = Column(Integer, info={'min': 1}) - cha = Column(Integer, info={'min': 1}) + str = Column(Integer, info={'min': 0, 'max': 30}) + dex = Column(Integer, info={'min': 0, 'max': 30}) + con = Column(Integer, info={'min': 0, 'max': 30}) + int = Column(Integer, info={'min': 0, 'max': 30}) + wis = Column(Integer, info={'min': 0, 'max': 30}) + cha = Column(Integer, info={'min': 0, 'max': 30}) diff --git a/ttfrog/webserver/controllers/base.py b/ttfrog/webserver/controllers/base.py index 0aa5f53..52d1043 100644 --- a/ttfrog/webserver/controllers/base.py +++ b/ttfrog/webserver/controllers/base.py @@ -1,11 +1,28 @@ import logging +import re from collections import defaultdict from wtforms_sqlalchemy.orm import model_form +from pyramid.httpexceptions import HTTPFound +from pyramid.interfaces import IRoutesMapper + from ttfrog.db.manager import db +def get_all_routes(request): + uri_pattern = re.compile(r"^([^\{\*]+)") + mapper = request.registry.queryUtility(IRoutesMapper) + routes = {} + 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 @@ -13,15 +30,15 @@ class BaseController: self.request = request self.attrs = defaultdict(str) self.record = None + self.form = None self.model_form = None self.config = { 'static_url': '/static', 'project_name': 'TTFROG' } - - self.configure() self.configure_for_model() + self.configure() def configure_for_model(self): if not self.model: @@ -29,7 +46,7 @@ class BaseController: if not self.model_form: self.model_form = model_form(self.model, db_session=db.session) if not self.record: - self.record = self.load_from_slug() or self.load_from_id() + self.record = self.get_record_from_slug() if 'all_records' not in self.attrs: self.attrs['all_records'] = db.query(self.model).all() @@ -37,50 +54,58 @@ class BaseController: def configure(self): pass - def load_from_slug(self): + def get_record_from_slug(self): if not self.model: return - - parts = self.request.matchdict.get('uri', '').split('/') + parts = self.request.matchdict.get('uri', '').split('-') if not parts: return + slug = parts[0].replace('/', '') + if not slug: + return try: - return db.query(self.model).filter(self.model.slug == parts[0])[0] + return db.query(self.model).filter(self.model.slug == slug)[0] except IndexError: - logging.warning(f"Could not load record with slug {parts[0]}") + logging.warning(f"Could not load record with slug {slug}") - def load_from_id(self): - post_id = self.request.POST.get('id', None) - if not post_id: - return - return db.query(self.model).get(post_id) - - def form(self) -> str: + def process_form(self): if not self.model: - return + return False if self.request.method == 'POST': + + # if we haven't loaded a record, we're creating a new one if not self.record: self.record = self.model() - form = self.model_form(self.request.POST, obj=self.record) - if self.model.validate(form): - form.populate_obj(self.record) + + # generate a form object using the POST form data and the db record + self.form = self.model_form(self.request.POST, obj=self.record) + if self.model.validate(self.form): + # update the record. If it's a record bound to the session + # updates will be commited automatically. Otherwise we must + # add and commit the record. + self.form.populate_obj(self.record) if not self.record.id: with db.transaction(): db.session.add(self.record) logging.debug(f"Added {self.record = }") - return form - return self.model_form(obj=self.record) + return True + return False + self.form = self.model_form(obj=self.record) + return False def output(self, **kwargs) -> dict: - return dict( + return dict(c=dict( config=self.config, request=self.request, - record=self.record or '', - form=self.form() or '', + form=self.form, + record=self.record, + routes=get_all_routes(self.request), **self.attrs, **kwargs, - ) + )) def response(self): + if self.process_form(): + return HTTPFound(location=f"{self.request.current_route_path}/{self.record.uri}") return self.output() diff --git a/ttfrog/webserver/routes.py b/ttfrog/webserver/routes.py index f6a8d63..d19df81 100644 --- a/ttfrog/webserver/routes.py +++ b/ttfrog/webserver/routes.py @@ -1,3 +1,3 @@ def routes(config): config.add_route('index', '/') - config.add_route('sheet', '/sheet/{uri:.*}', factory='ttfrog.webserver.controllers.CharacterSheet') + config.add_route('sheet', '/c{uri:.*}', factory='ttfrog.webserver.controllers.CharacterSheet')