fixing slugs

This commit is contained in:
evilchili 2024-02-02 15:40:45 -08:00
parent 9cdf28502a
commit 32d9c42847
10 changed files with 99 additions and 58 deletions

View File

@ -20,6 +20,9 @@ pyramid-jinja2 = "^2.10"
pyramid-sqlalchemy = "^1.6" pyramid-sqlalchemy = "^1.6"
wtforms-sqlalchemy = "^0.4.1" wtforms-sqlalchemy = "^0.4.1"
transaction = "^4.0" transaction = "^4.0"
unicode-slugify = "^0.1.5"
nanoid = "^2.0.0"
nanoid-dictionary = "^2.4.0"
[build-system] [build-system]

View File

@ -1,9 +1,9 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<title>{{ config['project_name'] }}{% block title %}{% endblock %}</title> <title>{{ c['config']['project_name'] }}{% block title %}{% endblock %}</title>
<meta name="og:provider_name" content="{{ config['project_name'] }}"> <meta name="og:provider_name" content="{{ c['config']['project_name'] }}">
<link rel='stylesheet' href="{{config['static_url']}}/styles.css" /> <link rel='stylesheet' href="{{c['config']['static_url']}}/styles.css" />
{% block headers %}{% endblock %} {% block headers %}{% endblock %}
</head> </head>
<body> <body>

View File

@ -3,19 +3,19 @@
{% block content %} {% block content %}
{{ build_list(all_records) }} {{ build_list(c) }}
<div style='float:left;'> <div style='float:left;'>
<h1>{{ record.name }}</h1> <h1>{{ c['record'].name }}</h1>
<form name="character_sheet" method="post" novalidate class="form"> <form name="character_sheet" method="post" novalidate class="form">
{{ form.csrf_token }} {{ c['form'].csrf_token }}
{% if 'process' in form.errors %} {% if 'process' in c['form'].errors %}
Error: {{ form.errors['process'] |join(',') }} Error: {{ c['form'].errors['process'] |join(',') }}
{% endif %} {% endif %}
<ul> <ul>
{% for field in form %} {% for field in c['form'] %}
<li>{{ field.label }}: {{ field }} {{ field.errors|join(',') }}</li> <li>{{ field.label }}: {{ field }} {{ field.errors|join(',') }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -27,9 +27,7 @@
{% block debug %} {% block debug %}
<div style='clear:both;display:block;'> <div style='clear:both;display:block;'>
<h2>Debug</h2> <h2>Debug</h2>
<h3>Record</h3> <pre>
<pre>{{ record }}</pre> {{ c }}
<h3>Config</h3> </pre>
<pre>{{ config }}</pre>
</div>
{% endblock %} {% endblock %}

View File

@ -1,9 +1,9 @@
{% macro build_list(records) %} {% macro build_list(c) %}
<div style='float:left; min-height: 90%; margin-right:5em;'> <div style='float:left; min-height: 90%; margin-right:5em;'>
<ul> <ul>
<li><a href="/sheet/">Create a Character</a></li> <li><a href="{{c['routes']['sheet']}}">Create a Character</a></li>
{% for rec in records %} {% for rec in c['all_records'] %}
<li><a href="/sheet/{{rec['slug']}}/{{rec['name']}}">{{ rec['name'] }}</a></li> <li><a href="{{c['routes']['sheet']}}/{{rec['uri']}}">{{ rec['uri'] }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

@ -1,5 +1,26 @@
import nanoid
from nanoid_dictionary import human_alphabet
from pyramid_sqlalchemy import BaseObject from pyramid_sqlalchemy import BaseObject
from wtforms import validators 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: class IterableMixin:
@ -50,4 +71,4 @@ class FormValidatorMixin:
# class Table(*Bases): # class Table(*Bases):
Bases = [BaseObject, IterableMixin, FormValidatorMixin] Bases = [BaseObject, IterableMixin, FormValidatorMixin, SlugMixin]

View File

@ -1,11 +1,9 @@
import logging import logging
import transaction
from ttfrog.db.manager import db from ttfrog.db.manager import db
from ttfrog.db import schema from ttfrog.db import schema
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.inspection import inspect
# move this to json or whatever # move this to json or whatever
data = { data = {
@ -33,7 +31,6 @@ def bootstrap():
try: try:
with db.transaction(): with db.transaction():
db.session.add(obj) db.session.add(obj)
obj.slug = db.slugify(rec)
except IntegrityError as e: except IntegrityError as e:
if 'UNIQUE constraint failed' in str(e): if 'UNIQUE constraint failed' in str(e):
logging.info(f"Skipping existing {table} {obj}") logging.info(f"Skipping existing {table} {obj}")

View File

@ -30,7 +30,7 @@ class SQLDatabaseManager:
def engine(self): def engine(self):
return create_engine(self.url) return create_engine(self.url)
@property @cached_property
def session(self): def session(self):
return Session return Session

View File

@ -2,7 +2,6 @@ from sqlalchemy import Column
from sqlalchemy import Integer from sqlalchemy import Integer
from sqlalchemy import String from sqlalchemy import String
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy import CheckConstraint
# from sqlalchemy import PrimaryKeyConstraint # from sqlalchemy import PrimaryKeyConstraint
# from sqlalchemy import DateTime # from sqlalchemy import DateTime
@ -14,20 +13,18 @@ class Ancestry(*Bases):
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, index=True, unique=True) name = Column(String, index=True, unique=True)
slug = Column(String, index=True, unique=True)
class Character(*Bases): class Character(*Bases):
__tablename__ = "character" __tablename__ = "character"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
slug = Column(String, index=True, unique=True)
ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False) ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False)
name = Column(String(255), nullable=False) name = Column(String(255), nullable=False)
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}) level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
str = Column(Integer, info={'min': 1}) str = Column(Integer, info={'min': 0, 'max': 30})
dex = Column(Integer, info={'min': 1}) dex = Column(Integer, info={'min': 0, 'max': 30})
con = Column(Integer, info={'min': 1}) con = Column(Integer, info={'min': 0, 'max': 30})
int = Column(Integer, info={'min': 1}) int = Column(Integer, info={'min': 0, 'max': 30})
wis = Column(Integer, info={'min': 1}) wis = Column(Integer, info={'min': 0, 'max': 30})
cha = Column(Integer, info={'min': 1}) cha = Column(Integer, info={'min': 0, 'max': 30})

View File

@ -1,11 +1,28 @@
import logging import logging
import re
from collections import defaultdict from collections import defaultdict
from wtforms_sqlalchemy.orm import model_form from wtforms_sqlalchemy.orm import model_form
from pyramid.httpexceptions import HTTPFound
from pyramid.interfaces import IRoutesMapper
from ttfrog.db.manager import db 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: class BaseController:
model = None model = None
@ -13,15 +30,15 @@ class BaseController:
self.request = request self.request = request
self.attrs = defaultdict(str) self.attrs = defaultdict(str)
self.record = None self.record = None
self.form = None
self.model_form = None self.model_form = None
self.config = { self.config = {
'static_url': '/static', 'static_url': '/static',
'project_name': 'TTFROG' 'project_name': 'TTFROG'
} }
self.configure()
self.configure_for_model() self.configure_for_model()
self.configure()
def configure_for_model(self): def configure_for_model(self):
if not self.model: if not self.model:
@ -29,7 +46,7 @@ class BaseController:
if not self.model_form: if not self.model_form:
self.model_form = model_form(self.model, db_session=db.session) self.model_form = model_form(self.model, db_session=db.session)
if not self.record: 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: if 'all_records' not in self.attrs:
self.attrs['all_records'] = db.query(self.model).all() self.attrs['all_records'] = db.query(self.model).all()
@ -37,50 +54,58 @@ class BaseController:
def configure(self): def configure(self):
pass pass
def load_from_slug(self): def get_record_from_slug(self):
if not self.model: if not self.model:
return return
parts = self.request.matchdict.get('uri', '').split('-')
parts = self.request.matchdict.get('uri', '').split('/')
if not parts: if not parts:
return return
slug = parts[0].replace('/', '')
if not slug:
return
try: 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: 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): def process_form(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:
if not self.model: if not self.model:
return return False
if self.request.method == 'POST': if self.request.method == 'POST':
# if we haven't loaded a record, we're creating a new one
if not self.record: if not self.record:
self.record = self.model() self.record = self.model()
form = self.model_form(self.request.POST, obj=self.record)
if self.model.validate(form): # generate a form object using the POST form data and the db record
form.populate_obj(self.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: if not self.record.id:
with db.transaction(): with db.transaction():
db.session.add(self.record) db.session.add(self.record)
logging.debug(f"Added {self.record = }") logging.debug(f"Added {self.record = }")
return form return True
return self.model_form(obj=self.record) return False
self.form = self.model_form(obj=self.record)
return False
def output(self, **kwargs) -> dict: def output(self, **kwargs) -> dict:
return dict( return dict(c=dict(
config=self.config, config=self.config,
request=self.request, request=self.request,
record=self.record or '', form=self.form,
form=self.form() or '', record=self.record,
routes=get_all_routes(self.request),
**self.attrs, **self.attrs,
**kwargs, **kwargs,
) ))
def response(self): def response(self):
if self.process_form():
return HTTPFound(location=f"{self.request.current_route_path}/{self.record.uri}")
return self.output() return self.output()

View File

@ -1,3 +1,3 @@
def routes(config): def routes(config):
config.add_route('index', '/') 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')