added transaction log, UX scaffolding

This commit is contained in:
evilchili
2024-02-08 01:14:35 -08:00
parent 2dcaa3fac6
commit da6255a86a
15 changed files with 460 additions and 73 deletions
+29 -5
View File
@@ -1,12 +1,10 @@
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
from sqlalchemy import String
from pyramid_sqlalchemy import BaseObject
from slugify import slugify
def genslug():
@@ -38,5 +36,31 @@ class IterableMixin:
return f"{self.__class__.__name__}: {str(dict(self))}"
def multivalue_string_factory(name, column=Column(String), separator=';'):
"""
Generate a mixin class that adds a string column with getters and setters
that convert list values to strings and back again. Equivalent to:
class MultiValueString:
_name = column
@property
def name_property(self):
return self._name.split(';')
@name.setter
def name(self, val):
return ';'.join(val)
"""
attr = f"_{name}"
prop = property(lambda self: getattr(self, attr).split(separator))
setter = prop.setter(lambda self, val: setattr(self, attr, separator.join(val)))
return type('MultiValueString', (object, ), {
attr: column,
f"{name}_property": prop,
name: setter,
})
# class Table(*Bases):
Bases = [BaseObject, IterableMixin, SlugMixin]
+61 -4
View File
@@ -7,13 +7,70 @@ from sqlalchemy.exc import IntegrityError
# move this to json or whatever
data = {
'CharacterClass': [
{
'name': 'fighter',
'hit_dice': '1d10',
'hit_dice_stat': 'CON',
'proficiencies': 'all armor, all shields, simple weapons, martial weapons',
'saving_throws': ['STR, CON'],
'skills': ['Acrobatics', 'Animal Handling', 'Athletics', 'History', 'Insight', 'Intimidation', 'Perception', 'Survival'],
},
{
'name': 'rogue',
'hit_dice': '1d8',
'hit_dice_stat': 'DEX',
'proficiencies': 'simple weapons, hand crossbows, longswords, rapiers, shortswords',
'saving_throws': ['DEX', 'INT'],
'skills': ['Acrobatics', 'Athletics', 'Deception', 'Insight', 'Intimidation', 'Investigation', 'Perception', 'Performance', 'Persuasion', 'Sleight of Hand', 'Stealth'],
},
],
'Skill': [
{'name': 'Acrobatics'},
{'name': 'Animal Handling'},
{'name': 'Athletics'},
{'name': 'Deception'},
{'name': 'History'},
{'name': 'Insight'},
{'name': 'Intimidation'},
{'name': 'Investigation'},
{'name': 'Perception'},
{'name': 'Performance'},
{'name': 'Persuasion'},
{'name': 'Sleight of Hand'},
{'name': 'Stealth'},
{'name': 'Survival'},
],
'Ancestry': [
{'name': 'human'},
{'name': 'dragonborn'},
{'name': 'tiefling'},
{'name': 'human', 'creature_type': 'humanoid'},
{'name': 'dragonborn', 'creature_type': 'humanoid'},
{'name': 'tiefling', 'creature_type': 'humanoid'},
],
'Character': [
{'id': 1, 'name': 'Sabetha', 'ancestry': 'tiefling', 'level': 10, 'str': 10, 'dex': 10, 'con': 10, 'int': 10, 'wis': 10, 'cha': 10},
{
'id': 1,
'name': 'Sabetha',
'ancestry': 'tiefling',
'character_class': ['fighter', 'rogue'],
'level': 1,
'armor_class': 10,
'max_hit_points': 14,
'hit_points': 14,
'temp_hit_points': 0,
'passive_perception': 10,
'passive_insight': 10,
'passive_investigation': 10,
'speed': '30 ft.',
'str': 16,
'dex': 12,
'con': 18,
'int': 11,
'wis': 12,
'cha': 8,
'proficiencies': 'all armor, all shields, simple weapons, martial weapons',
'saving_throws': ['STR', 'CON'],
'skills': ['Acrobatics', 'Animal Handling'],
},
]
}
+73 -8
View File
@@ -1,32 +1,97 @@
import enum
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import ForeignKey
# from sqlalchemy import PrimaryKeyConstraint
# from sqlalchemy import DateTime
from sqlalchemy import Enum
from sqlalchemy import Text
from ttfrog.db.base import Bases
from ttfrog.db.base import Bases, BaseObject, IterableMixin
from ttfrog.db.base import multivalue_string_factory
class Ancestry(*Bases):
__tablename__ = "ancestry"
STATS = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
name = Column(String, primary_key=True, unique=True)
CREATURE_TYPES = ['aberation', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'Giant',
'humanoid', 'monstrosity', 'ooze', 'plant', 'undead']
# enums for db schemas
StatsEnum = enum.Enum("StatsEnum", ((k, k) for k in STATS))
CreatureTypesEnum = enum.Enum("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
CharacterClassMixin = multivalue_string_factory('character_class', Column(String, nullable=False))
SavingThrowsMixin = multivalue_string_factory('saving_throws')
SkillsMixin = multivalue_string_factory('skills')
class Skill(*Bases):
__tablename__ = "skill"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, index=True, unique=True)
description = Column(Text)
def __repr__(self):
return str(self.name)
class Character(*Bases):
__tablename__ = "character"
class Proficiency(*Bases):
__tablename__ = "proficiency"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, index=True, unique=True)
def __repr__(self):
return str(self.name)
class Ancestry(*Bases):
__tablename__ = "ancestry"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, index=True, unique=True)
creature_type = Column(Enum(CreatureTypesEnum))
def __repr__(self):
return str(self.name)
class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
__tablename__ = "character_class"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, index=True, unique=True)
hit_dice = Column(String, default='1d6')
hit_dice_stat = Column(Enum(StatsEnum))
proficiencies = Column(String)
def __repr__(self):
return str(self.name)
class Character(*Bases, CharacterClassMixin, SavingThrowsMixin, SkillsMixin):
__tablename__ = "character"
id = Column(Integer, primary_key=True, autoincrement=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})
armor_class = Column(Integer, nullable=False, info={'min': 1, 'max': 99})
hit_points = Column(Integer, nullable=False, info={'min': 0, 'max': 999})
max_hit_points = Column(Integer, nullable=False, info={'min': 0, 'max': 999})
temp_hit_points = Column(Integer, nullable=False, info={'min': 0})
passive_perception = Column(Integer, nullable=False)
passive_insight = Column(Integer, nullable=False)
passive_investigation = Column(Integer, nullable=False)
speed = Column(String, nullable=False, default="30 ft.")
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})
proficiencies = Column(String)
class TransactionLog(BaseObject, IterableMixin):
__tablename__ = "transaction_log"
id = Column(Integer, primary_key=True, autoincrement=True)
source_table_name = Column(String, index=True, nullable=False)
primary_key = Column(Integer, index=True)
diff = Column(Text)
+36
View File
@@ -0,0 +1,36 @@
import json
import logging
from ttfrog.db.manager import db
from ttfrog.db.schema import TransactionLog
def record(previous, new):
diff = list((set(previous.items()) ^ set(dict(new).items())))
if not diff:
return
rec = TransactionLog(
source_table_name=new.__tablename__,
primary_key=new.id,
diff=json.dumps(diff),
)
with db.transaction():
db.add(rec)
logging.debug(f"Saved restore point: {dict(rec)}")
return rec
def restore(rec, log_id=None):
if log_id:
log = db.query(TransactionLog).filter_by(id=log_id).one()
else:
log = db.query(TransactionLog).filter_by(source_table_name=rec.__tablename__, primary_key=rec.id).one()
logging.debug(f"Located restore point {log = }")
diff = json.loads(log.diff)
updates = dict(diff[::2])
if not updates:
return
logging.debug(f"{updates = }")
with db.transaction():
db.query(db.tables[log.source_table_name]).update(updates)